Stack trace analysis
Teach Agents to find the first business frame from bottom to top, recognize wrapper exceptions and async boundaries, and handle minification and missing source maps.
A SKILL should state: root cause often lies at the earliest throw or innermost Caused by, not the last framework line in logs. Require expanding suppressed and aggregate exceptions.
Give field meanings per stack format (JS, JVM, Go, .NET); production needs commit SHA, build id, and source maps to map to source lines.
Async and thread pools scramble stacks: combine with trace_id, thread names, or reactive context to rebuild the chain; do not change unrelated modules from the top frame alone.
- Confirm the stack matches the currently deployed version before deep analysis.
- Distinguish OOM and other truncated or empty stacks—pivot to system logs and metrics.
- Output should list suspicious frames, files to open, and suggested log points.
Frames: order, causality, noise
Top of stack is usually the last entered call (or async callback entry); walking down approaches the original trigger; JVM / .NET print order may differ from the JS console—state in the SKILL which end is “deeper”.
Async code stack differences — Promise chain vs async/await:
// ===== Promise chain stack (noisy, hard to trace causality) =====
Error: Payment failed
at chargeCard (/app/src/payment.js:45:13) // <- actual throw point
at processTicksAndRejections (node:internal/process/task_queues:95:5)
// Promise chain loses the handleCheckout frame (async-scheduled, caller exits)
// ===== async/await stack (V8 async stack traces) =====
Error: Payment failed
at chargeCard (/app/src/payment.js:45:13)
at async processPayment (/app/src/payment.js:28:5)
at async handleCheckout (/app/src/checkout.js:15:3)
at async Router.post (/app/src/routes.js:42:5)
// Stack noise filter (remove framework layers)
function isBusinessFrame(frame) {
if (frame.includes('node:internal') || frame.includes('node_modules')) {
return frame.includes('@company/') || frame.includes('sequelize');
}
return frame.includes('/src/') || frame.includes('/app/');
}
const businessFrames = stackTrace.split('\n')
.filter(line => line.trim().startsWith('at '))
.filter(isBusinessFrame);
- Causal frames: package/path in your repo, tied to changes or feature flags—check version and branch first.
- Noise frames: reflection, AOP, test harness, Promise microtasks, webpack / vite runtime—can fold but do not edit library code before reading business frames.
-
Wrapped exceptions: outer
RuntimeExceptionoraggregateErrormay hide inner ones—expandCaused by,errors[],suppressed.
Common wrapped-exception unwrapping (Java getCause / Python __cause__):
// Java: recursively unwrap Caused by chain
// RuntimeException wraps SQLTimeoutException wraps SocketTimeoutException
// Root cause is the innermost: SocketTimeoutException
public static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable.getCause();
while (cause != null && cause.getCause() != null) {
cause = cause.getCause();
}
return cause != null ? cause : throwable;
}
# Python: __cause__ and __context__
try:
result = db.query("SELECT ...")
except DatabaseError as e:
raise PaymentError("Payment failed") from e # explicit chain: __cause__
def get_root_cause(exc):
while exc.__cause__ is not None:
exc = exc.__cause__
return exc
import traceback
traceback.print_exc() # "The above exception was the direct cause of..."
// JavaScript: AggregateError unwrap (Promise.any)
try {
await Promise.any([fetchA(), fetchB(), fetchC()]);
} catch (err) {
if (err instanceof AggregateError) {
err.errors.forEach((e, i) => console.error('Promise', i, 'failed:', e.stack));
}
}
Source maps and production symbolication
After bundling, minification, and tree-shaking, stacks often show .min.js or hashed chunks; without sourceMappingURL or uploaded maps, line numbers refer to build artifacts, not TypeScript / Kotlin sources.
- A SKILL should require errors to carry release / commit / build id bound to CI artifacts or platform symbol files.
- Front end: distinguish “browser mapped stack” vs “raw stack in server logs”—they may differ.
- Native and backend: dSYM, ProGuard mapping, Go inline tables, etc.—document per project so Agents do not assume TS line numbers always exist.
Correct handling of Node.js uncaught exceptions and unhandled Promise rejections:
// Node.js global exception handling (register at app entry)
process.on('uncaughtException', (err, origin) => {
logger.error('Uncaught exception', {
error: { type: err.constructor.name, message: err.message, stack: err.stack },
origin,
trace_id: getCurrentTraceId(),
});
gracefulShutdown().finally(() => process.exit(1));
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled promise rejection', {
reason: reason instanceof Error
? { type: reason.constructor.name, message: reason.message, stack: reason.stack }
: reason,
});
// Node.js 15+: crashes by default, consistent with uncaughtException
});
// Better: use async_hooks to track async context
import { AsyncLocalStorage } from 'async_hooks';
const requestContext = new AsyncLocalStorage();
app.use((req, res, next) => {
requestContext.run({ traceId: req.headers['x-trace-id'] }, next);
});
Analysis flow (skill-flow-block)
[ Collect: full stack + Caused by / aggregate + deploy version / commit ]
│
▼
[ Identify format: V8 / JVM / Go / .NET; confirm stack growth direction ]
│
▼
[ Strip noise: runtime, framework glue, test fixtures, min paths mappable? ]
│
▼
[ Lock causality: first business frame + innermost root exception ]
│
┌────────┴────────┐
▼ ▼
[ Have source map / symbols ] [ No mapping: artifact line numbers only ]
│ │
▼ ▼
[ Map to source file:line:col ] [ Use build artifact or chunk name + version ]
│ │
└────────┬────────┘
▼
[ Hypothesis: input, concurrency, config, deps; list validation steps and log points ]
Frame line extractor (demo)
A local demo lightweight parser: regex-extract path/class + line from common stack text so a SKILL can say “structure first, then reason”. Not a full language parser.
Supports common at … (file:line:col), at file:line:col, and (Foo.java:line); unmatched lines are skipped.
Summary
Stack analysis hinges on direction, causal chain, and version mapping: confirm stack order and runtime, land on business frames and the innermost exception, then use source maps / symbols to map lines to editable source; the demo extractor only helps structure, not replace platform symbolication.
---
name: stack-trace-analysis
description: Read exception stacks and Caused-by chains
model: claude-sonnet-4-5
---
# Steps
steps:
1. Collect full stack (with Caused by/aggregate/suppressed) + deploy version/commit
2. Identify stack format (V8/JVM/Go/.NET) and growth direction
3. Strip noise frames (runtime/framework/test fixtures/min paths)
4. Unwrap wrapped exceptions (getCause/errors[]/suppressed recursively)
5. Lock in first business frame and innermost root exception
6. Confirm source map / symbol availability
7. Map to source file line numbers
8. Propose testable hypotheses and suggested log points
# Async stack notes
async_stack_notes:
- async/await preserves full call chain (better than Promise chain)
- Promise chain may lose intermediate frames
- Node.js: --async-context-frame-depth increases trace depth
- Thread pool / Worker: combine with trace_id to associate context
# Frame classification rules
frame_classification:
business: frames with /src/ /app/ @company/ paths
noise: node:internal/ node_modules/ .min.js webpack-runtime
exception: unwrap Caused by / __cause__ / AggregateError.errors[]