A stack trace is a witness, not a verdict. The frame at the top is where the program died. It is rarely where the bug lives. Senior engineers triage traces by jumping past the death and looking for the decision — the line where the system already had the wrong state but hadn't yet noticed. This post walks through the decision flow they use, the patterns that hide the real cause, and the moments when you should ignore the trace entirely.
Why this matters
Stack traces are the highest-density debugging signal you get for free, and they're also the most misread artifact in any codebase. The cost of getting them wrong isn't just slow triage — it's a confident fix to the wrong line, a regression closed without root cause, and a recurring incident in the on-call queue six weeks later. The reading discipline below is small, but it is the thing that separates engineers who close incidents from engineers who close causes.
What do you look at first — and why it's not the top frame?
The first thing to look at is the boundary — the lowest frame in your own code before the trace leaves into a library, runtime, or framework. The top frame is almost always library code, and library code rarely has the bug. It just had the misfortune of being holding the bad state when the assertion fired.
Consider this trace from a Node service:
TypeError: Cannot read properties of undefined (reading 'id')
at Array.map (<anonymous>)
at serializeUsers (/app/src/serializers/user.ts:18:21)
at UserController.list (/app/src/controllers/users.ts:42:14)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
The top frame is Array.map. The bug is not in Array.map. The real frame to read is serializeUsers on line 18 — that's the lowest frame in your code. The question to ask isn't "what does .map do with undefined" — it's "why did the array serializeUsers received contain an undefined user?"
The decision tree below is the one we teach in the platform's debugging track. It catches the actual cause in roughly 80% of cases. The remaining 20% need the async or boundary analysis discussed in the next sections.
How do you spot async boundaries that hide the real cause?
Async code lies about causality. When a Promise.then or an await resumes, the stack you see is the stack at resumption, not the stack that scheduled the work. In practice this means the call that caused the failure is often not in the trace at all.
The pattern to recognize: a stack trace that bottoms out in processTicksAndRejections, setImmediate, runMicrotasks, or any scheduler primitive, with no application frames between the scheduler and the failing line. That trace is telling you "the bad work was dispatched from somewhere I can't show you." The next step is not to stare at the trace harder. It is to grep for the dispatch site — every place that resolves into the function in the top frame — and reproduce with logging at the dispatch points.
Distributed traces are the same problem, one layer up. When you cross a service boundary, the in-process trace ends. You need the span from the upstream service to continue the causal chain. If you don't have it, you are debugging blind.
When should you ignore the stack trace entirely?
Three situations. First, when the trace crosses a deserializer — JSON, protobuf, a database row mapper. The trace will point at the deserializer, but the bug is the payload. Read the raw input before the trace. Second, when the failure is downstream of a retry. Retries flatten the original failure into whatever the last attempt happened to hit, and the trace tells you about the last attempt, not the first one. Third, when the error is out of memory or any resource exhaustion. The trace shows what was holding the heap at the moment of death, which is rarely what filled it.
The frequency distribution below is from a sample of 1,400 triaged incidents on the Codritium platform. We logged where the root cause actually lived relative to the top frame.
The bottom row matters. In 8% of incidents, the bug wasn't in the trace at all — it was dispatched from somewhere asynchronous, hidden behind a retry, or upstream of a service boundary. If you anchor entirely on the trace, those 8% become recurring incidents.
The table below captures the patterns we see most often, with the modal cause and the red herring to ignore.
| Trace pattern | Modal cause | Check first | Common red herring |
|---|---|---|---|
| Top frame is Array/Map/Object method | Bad input to your wrapper function | Lowest application frame, inspect array contents | The method itself — it has no bug |
| Trace ends in scheduler primitive | Dispatch site of an async task | Grep callers of the failing function | The resumed frame — it lost the original stack |
| Crosses a deserializer | Malformed or null payload upstream | Raw bytes / request log | The parser — it's reporting, not failing |
| OOM / heap exhausted | Allocation pattern earlier in the request | Heap snapshot, recent allocation traces | Whatever was running at OOM time |
| After a retry | Original failure, masked by retry policy | First-attempt log, not last-attempt trace | The trace itself — it is the wrong attempt |
| Cross-service RPC error | Upstream contract drift or auth scope change | Upstream span and recent deploys | Local error mapper — it just relays |
What does a senior do differently from a junior here?
The shortest version: a senior reads the trace last. They start with the incident description, form a hypothesis about what kind of failure it is — input, state, contract, resource — and then use the trace to confirm or kill the hypothesis. A junior reads the trace first and tries to derive a hypothesis from it. The trace-first approach is faster on simple bugs and disastrous on the patterns above, because the trace will obediently answer whichever question you ask of it.
The second difference is what they do when the fix is obvious. A junior fixes the line and closes the ticket. A senior asks: what would have prevented the bad state from reaching this line in the first place? That question is the difference between fixing the symptom and fixing the cause, and it is the line beneath which incidents become recurring.
Why 'fix the symptom' lands you in the regression-rate study's bottom quartile.
From Codritium Research
Regression Rate by Skill Band
How often a 'fix' introduces a new bug, segmented across Easy / Medium / Hard sessions and twelve task categories. The fix-then-regress feedback loop, charted.
Common questions
Are sourcemaps always reliable?
No. Production sourcemaps lie when build artifacts and uploaded maps drift, when minifier name collisions resolve to the wrong original symbol, or when inline transformations (like CSS-in-JS or templating) sit between source and bundle. Trust the release artifact hash, not the rendered frame name.
How do you handle minified production traces?
Get the build artifact for the exact release that produced the trace and resolve frames against it locally. If you don't have the artifact, the only reliable signal is the line/column position relative to the bundle — symbol names are guesses. This is why immutable, hash-addressed build artifacts are non-negotiable.
Should you use AI to read traces?
For pattern recognition — async boundaries, deserializer hops, retry wrappers — yes. For root cause — no. Models will confidently fabricate a causal story from any trace, and the story will be internally consistent. Use AI to enumerate candidate causes; pick the cause yourself by reading the code.
What about distributed traces across services?
Treat the in-process stack as one segment in a larger causal chain. The bug is rarely in the service that observed the failure — it is more often in the service that produced the bad payload one or two hops upstream. Spans, not stacks, are the unit of analysis here.
How do you avoid 'fixing the symptom'?
Write the post-fix question explicitly: 'what state could have prevented the bad input from reaching this line?' If your fix can't answer that, you patched the symptom. The regression-rate study shows this single discipline accounts for most of the variance between top-quartile and bottom-quartile engineers on the rubric.
Where to go next
- Regression Rate by Band — the data behind 'fix the cause, not the symptom'
- Where AI Pair Programming Fails — including the trace-reading failure modes
- The Defensibility Rubric 2026 — how root-cause analysis is scored on the platform