How to Read a Stack Trace End to End

Written By  Crosscheck Team

Content Team

May 28, 2026 8 minutes

How to Read a Stack Trace End to End

How to read a stack trace end to end

You click "Save" on the checkout page and the screen goes blank. In the console there are forty lines of red text, most of them pointing at files you have never opened. Your eyes glaze over and you scroll back to the top.

That wall of text is a stack trace, and almost all of it is noise. Usually one line tells you where the bug is.

Short version

  • A stack trace is the list of function calls that were running when the error happened, newest first.
  • The first line is the error message and type. The lines below it are called frames.
  • The frame that matters is usually the topmost one that points at your code, not a library.
  • Framework frames can be hidden with the DevTools ignore list, which makes the useful frames obvious.
  • Async code splits traces in half. If the trace ends at a timer or a fetch, the cause is in a different trace.
  • Always copy the whole trace when you report a bug. The line you skip is often the one a developer needs.

What a stack trace actually is

When your code calls a function, the browser writes that function on a stack — a pile of "what am I doing right now" notes. When the function finishes, the note is removed. When an error is thrown, the browser prints the whole pile.

So a stack trace is a history of how the program arrived at the failure, read from the inside out.

Here is a real one:

TypeError: Cannot read properties of undefined (reading 'total')
    at formatSummary (checkout.js:112:24)
    at renderOrder (checkout.js:78:10)
    at handleSubmit (CheckoutForm.jsx:45:5)
    at HTMLButtonElement.callCallback (react-dom.development.js:4164:14)
    at invokeGuardedCallbackDev (react-dom.development.js:4213:16)

Five lines under the message, and only three of them are yours.


The anatomy of one frame

Every frame follows the same shape:

    at formatSummary (checkout.js:112:24)
PartMeaning
atJust a separator. Ignore it.
formatSummaryThe function that was running.
checkout.jsThe file it lives in.
112The line number.
24The column number — how many characters across the line.

The column number matters more than people think. On a line like order.items.map(i => i.price.total) there are four places that could throw. The column tells you which one.

If you see <anonymous> instead of a name, the function had no name — usually an arrow function passed inline. If you see Object.<anonymous>, the code was running at the top level of a module.


Read top-down first, bottom-up second

Two readings answer two different questions.

Top-down answers "where did it break?" The first frame is the deepest point the program reached. Start there. In the example above, formatSummary at checkout.js:112 is where the crash happened.

Bottom-up answers "why was that code running?" The last frame is where the chain started — a click handler, a route change, a page load. Reading upward tells you the path. In the example, a button click went to handleSubmit, which called renderOrder, which called formatSummary.

Use top-down to find the fix. Use bottom-up when the fix is not obvious and you need to know which input caused it.

Bad habit: reading only the message and guessing. Good habit: reading the message, then the first frame in your own code, then the caller above it.


Which frame actually matters

The first frame is where the error surfaced. That is not always where the bug is.

Take the trace above. The error is Cannot read properties of undefined (reading 'total'). That means something was undefined when the code tried to read .total from it. formatSummary did nothing wrong except trust its input. The real bug is probably in renderOrder, which passed a bad value.

A simple rule that works most of the time:

  1. Find the topmost frame that points at a file you can open and edit.
  2. Look at that line. Ask: is the error about bad input, or bad logic here?
  3. If it is bad input, move one frame down the list to the caller and repeat.
  4. Stop when you find the frame that created the bad value.

This is why traces where every frame belongs to a library are frustrating — the bug is still yours, but the evidence starts further back.


Framework noise and the ignore list

Modern apps produce traces where two thirds of the frames come from React, Vue, Angular, webpack, or a polyfill. These frames are almost never where your bug lives.

Chrome DevTools has a feature for this: the ignore list. Files on it are collapsed out of stack traces and skipped when you step through code in the debugger.

To set it up in Chrome:

  1. Open DevTools and go to Settings (the gear icon) then Ignore List.
  2. Confirm that "Add content scripts to ignore list" and "Add anonymous scripts" are on.
  3. Add a pattern such as /node_modules/ and /vendor/.
  4. Reload and reproduce the error.

You can also right-click any frame in the Console or Sources panel and pick Add script to ignore list. Firefox has the same idea under "Blackbox script".

After this, the earlier trace collapses to:

TypeError: Cannot read properties of undefined (reading 'total')
    at formatSummary (checkout.js:112:24)
    at renderOrder (checkout.js:78:10)
    at handleSubmit (CheckoutForm.jsx:45:5)
    (2 frames hidden by ignore list)

Three lines. All yours. This one setting saves more time than any other DevTools tweak.


Async boundaries: where traces go missing

This is the part that confuses people most.

When code runs later — inside setTimeout, a promise, an event handler, or an await — the original call stack is already gone. The browser cannot show you what set the timer, because that function finished long ago.

So you get a short, useless trace:

TypeError: Cannot read properties of null (reading 'id')
    at loadProfile (profile.js:34:19)

One frame. Nothing about who called loadProfile.

Three things help:

  • Async stack traces. Chrome, Firefox, and Node stitch the pieces together and mark the join with at async or a divider line. In Chrome this is on by default when DevTools is open. Look for text like --- await --- in the trace and keep reading below it.
  • Use await, not .then() chains. Traces across await are much better stitched than traces across nested callbacks.
  • Catch and re-throw with context. If a low-level function can fail, wrap it:
try {
  await savePayment(orderId);
} catch (err) {
  throw new Error(`savePayment failed for order ${orderId}`, { cause: err });
}

The cause option keeps the original error and its trace attached, so you get both halves. Node and every current browser support it.

One more async trap: an error inside a promise that nobody catches shows up as Uncaught (in promise) and is not caught by try/catch around the calling code. If you see that prefix, the missing piece is a .catch() somewhere.


A worked example, start to finish

The user report says: "Order summary is blank for the Acme account on https://staging.example.com/checkout."

  1. Reproduce with DevTools already open. Traces are only recorded once the panel is listening.
  2. Read the message. TypeError: Cannot read properties of undefined (reading 'total'). Something is undefined.
  3. Find the top frame in your code. formatSummary (checkout.js:112:24).
  4. Open that line. Click the file link in the console — it jumps straight there. Line 112 reads return order.pricing.total.toFixed(2);.
  5. Decide which value is missing. The message says the failing read was .total, so order.pricing was undefined, not order.
  6. Move down one frame. renderOrder (checkout.js:78) builds the order object from the API response.
  7. Check the network tab. The /api/orders/8821 response has no pricing key — the account is on a legacy plan.
  8. Now you have a real bug: the API omits pricing for legacy plans, and the UI assumes it always exists.

The fix might be in the API or the UI, but you can now describe the problem in one sentence instead of pasting red text into a chat.


Sharing a trace so it stays useful

Screenshots of stack traces are a small cruelty. They cannot be searched, copied, or clicked.

When you hand a trace to someone else, include:

  • The full trace as text, from the error message to the last frame. Right-click in the Console and choose "Copy" or use "Save as..." to keep everything.
  • The build or commit the app was running.
  • The URL and the account, such as [email protected] on https://staging.example.com/checkout.
  • Whether the trace came from minified code. If file names look like main.4f2a.js:1:88213, say so — a developer will need source maps to read it.

If gathering that context by hand is what stops people from doing it, a browser-based bug reporting tool such as Crosscheck captures the console output, network requests, and environment details with the report, so the trace arrives with the information needed to read it.


Frequently asked questions

Why does my stack trace only have one line? The error happened in async code, so the earlier frames were already cleared. Turn on async stack traces in DevTools, or wrap the failing call and re-throw with { cause: err } to keep the original trace.

What does <anonymous> mean in a frame? The function had no name, usually because it was an inline arrow function such as a .map() callback. The file, line, and column still point to the exact spot.

Are the file names in production traces useless? Only without source maps. Minified names like t.n and main.min.js:1:45823 become real names and lines once source maps are uploaded to your error tracker.

Should I read the trace from the top or the bottom? Top first, to find where it broke. Bottom second, to understand why that code was running. Both readings take under a minute.

Does the column number really matter? Yes, when a line does several things. On user.profile.settings.theme the column tells you which link in the chain was missing, which saves you from guessing.

Related Articles

Contact us
to find out how this model can streamline your business!

Trusted by thousands ofengineering teams worldwide.

Add to Chrome
200+ reviews · 100k+ users
Crosscheck browser extension capture controls

Join the Crosscheck Community

Stay in the loop with Crosscheck's newest features and insights.