Staring at a wall of red text and not sure where to look? Here is how to read a stack trace: read the error line first, then find the first frame in your own code.
This guide is part of the series on debugging techniques, where each piece covers a habit you can reuse on any codebase.
| Item | Value |
|---|---|
| Frames V8 keeps by default | 10 frames |
| Async stack traces on by default in V8 since | V8 v7.3 |
| Frame order in most languages | Most recent to oldest, top to bottom |
| Frame order in Python | Oldest first, error last |
| Where the investigation usually starts | First frame in a file you own |
These figures come from OpenReplay’s guide to JavaScript stack traces and Coding the Wheel’s article on reading a stack trace. Runtimes change between releases, so check the sources before you rely on them.
What a stack trace actually is
A stack trace is a record of the call stack at the moment something failed. It names the function that threw, its caller, that caller’s caller, and so on down to the entry point, as Rollbar’s Java guide describes it.
Each line in the trace is a frame. A frame carries the function name, the file, the line and, in JavaScript, the column. Java prints each frame in the shape package.Class.method(File.java:lineNumber), according to the same Rollbar guide.
The part that trips people up is order. In most languages the trace runs from most recent to oldest as you read down, but Coding the Wheel points out that Python is the notable inverse: it prints oldest first with the error last.
Before you read any frame, check which way your language prints:
- Java and JavaScript (V8): the top frame is where the error was thrown.
- Python: the bottom frame is where the error was thrown.
- Android: it follows the Java layout, so the top is the most recent call.
Read the exception line before the frames
The first thing to read is the exception type and message. They often hold the answer: a null pointer error tells you something was null, and a good message tells you which thing.
Coding the Wheel notes that Java’s helpful NullPointerExceptions and Python’s fine-grained error locations will often name the exact expression. That is why it recommends reading the whole message before you scroll. You can verify this in the original article.

Once you have the exception, resist the urge to patch the first plausible line. The same article advises reading the whole trace once before you change anything, because the urge to fix fast wastes more time than reading costs.
Why is the top frame usually not your bug?
The top frame is usually a library or built-in failing correctly on input your code handed it. Coding the Wheel puts it this way: the library is rarely wrong, and the interesting question is where the bad value came from.
OpenReplay walks through a JavaScript example. The chain reads as JSON.parse threw, parseCart called it, loadCart called parseCart, and so on down. The parser is only reporting that it received a bad string; the fix lives in the code that supplied it.
So scan for the first frame in a file you own. Skip past paths that point at:
- node_modules or vendor folders
- site-packages in Python
- java.base and other runtime prefixes
- anonymous or native frames
Coding the Wheel says that first frame of yours is almost always where the investigation starts. Open that line, find the call that failed, then ask where its argument came from by moving to the frames that supplied the value.
Follow “Caused by” down to the real failure
Many applications catch an exception, wrap it in a new one for context and rethrow it. The trace then shows a chain, and the top exception is only the wrapper. Rollbar’s example has a RuntimeException that says the configuration failed to load, with a FileNotFoundException underneath saying the file was missing.
Coding the Wheel sums up the rule: the top section is the symptom, and the last “caused by” is the cause. People who fix only the top of the trace are surprised when the failure comes back.
If you see a line ending in “… 1 more”, nothing is hidden from you. Rollbar explains that the remaining frames are identical to the outer trace, and Java collapses them to keep the output shorter.
JavaScript has a catch of its own. According to OpenReplay, Error.cause preserves the original error, but the engine does not merge the stacks: err.stack shows only the wrapper, so you have to look at the cause’s own stack.
Async, minified and truncated traces
Async code is where traces start to lie by omission. OpenReplay says V8 rebuilds async frames from the points where each await paused and resumed, so await, Promise.all() and Promise.any() are stitched together while a bare .then() chain leaves a gap. If the frames stop at an async boundary, look for a missing await in the layer above.
The same guide reports that async stack traces have been on by default since V8 v7.3, and that V8 keeps only 10 frames by default. That cap is adjustable through the non-standard Error.stackTraceLimit, per OpenReplay, so a deep trace may simply have been cut off.
When the origin is missing altogether, Coding the Wheel suggests attaching context yourself: a request or correlation ID logged at entry and included in every log line. If a failure shows up only now and then and the frames look innocent, read how to debug race conditions next, as in this guide on how to debug race conditions.
Production bundles add another problem. Minified traces show single-letter function names, and recovering the real names requires a source map that your tooling can reach, generated at build time. Coding the Wheel calls unreadable production traces a build problem worth fixing once rather than working around forever.
A routine you can repeat
Put the pieces together and you get a short routine that works in most languages:
- Read the exception type and the whole message.
- Work out which direction your language prints frames.
- Find the first frame in a file you own.
- Check for a “Caused by” block and jump to the last one.
- Walk the frames that supplied the bad value, and note any async gaps.
- Confirm that you are reading the deployed revision, not a drifted checkout.
The last step matters more than it looks. Coding the Wheel warns that line numbers drift from what you have checked out, so confirm you are reading the deployed revision.
If the trace still makes no sense, say it aloud, frame by frame, to someone or something that cannot interrupt you. The habit behind rubber duck debugging is often enough to expose the call you assumed was fine.
Frequently asked questions
Which line of a stack trace should I read first?
Show answer
Read the exception type and message first. Then find the first frame that points at a file you own, because Coding the Wheel says that frame is almost always where the investigation starts.
Do I read a stack trace from the top or the bottom?
Show answer
It depends on the language. In most languages the top is the most recent call, but Python prints oldest first with the error last, so you start at the bottom.
What does “Caused by” mean in a trace?
Show answer
It marks an exception that was wrapped by another one. The top exception is the symptom, and the last “Caused by” block holds the cause, according to Coding the Wheel.
Why are frames missing from my trace?
Show answer
Either the async chain broke or the trace hit a cap. OpenReplay says V8 keeps 10 frames by default and stitches async frames only at await points, Promise.all() and Promise.any().



