How to Read and Understand JavaScript Stack Traces
Learn how to read JavaScript stack traces, understand each line of an error output, and trace bugs back to their root cause. Covers browser and Node.js stack traces with real-world debugging examples.
When JavaScript throws an error, it prints a stack trace: a record of the function calls that led to the error. For beginners, stack traces look like a wall of cryptic text. For experienced developers, they are the single most useful debugging tool, pointing directly to the line of code that caused the problem and the path that led there.
This tutorial teaches you how to read every part of a JavaScript stack trace, understand what each line means, trace errors back to their root cause, and handle the differences between browser and Node.js trace formats. By the end, you will be able to look at any stack trace and know exactly where to start debugging.
What Is a Stack Trace?
A stack trace is a snapshot of the call stack at the moment an error occurred. The call stack is a data structure that tracks which functions are currently running. When function A calls function B, and B calls function C, the call stack looks like:
C ← currently executing (top of stack)
B ← called C
A ← called B
When an error occurs inside C, the stack trace records this chain from top (where the error happened) to bottom (where the program entry point was). This tells you not just where the error occurred, but how the code got there.
Anatomy of a Stack Trace
Here is a typical stack trace from a Chrome browser:
Uncaught TypeError: Cannot read properties of undefined (reading 'email')
at formatUserCard (app.js:47:28)
at renderUserList (app.js:31:14)
at loadDashboard (app.js:18:5)
at HTMLButtonElement.<anonymous> (app.js:7:3)
Every stack trace has two parts:
Part 1: The Error Message
Uncaught TypeError: Cannot read properties of undefined (reading 'email')
This tells you three things:
| Component | Value | Meaning |
|---|---|---|
| Error status | Uncaught | The error was not caught by a try-catch block |
| Error type | TypeError | A value was used in a way it does not support |
| Error detail | Cannot read properties of undefined (reading 'email') | Code tried to access .email on something that is undefined |
Part 2: The Call Stack Frames
Each line after the error message is a stack frame. Read them from top to bottom:
at formatUserCard (app.js:47:28) ← Frame 1: where the error happened
at renderUserList (app.js:31:14) ← Frame 2: who called formatUserCard
at loadDashboard (app.js:18:5) ← Frame 3: who called renderUserList
at HTMLButtonElement.<anonymous> (app.js:7:3) ← Frame 4: the event handler that started it all
Each frame follows the format:
at functionName (fileName:lineNumber:columnNumber)
| Part | Meaning |
|---|---|
functionName | The name of the function executing at that point |
fileName | The source file containing the function |
lineNumber | The line in the file (1-indexed) |
columnNumber | The column position on that line (1-indexed) |
Reading the Stack Trace Top-to-Bottom
The top frame is where the error physically occurred. But the root cause is often several frames down. In the example above:
- Frame 1 (
formatUserCard): This is where.emailwas accessed onundefined. But this function might be correct; it expected a valid user object. - Frame 2 (
renderUserList): This calledformatUserCard. Did it pass the wrong argument? - Frame 3 (
loadDashboard): This loaded the data. Did the API return unexpected data? - Frame 4 (click handler): This initiated the process. Was it called at the wrong time?
The fix might be in any of these frames. The stack trace gives you the investigation path.
Common JavaScript Error Types in Stack Traces
Understanding the error type immediately narrows down possible causes:
| Error Type | Meaning | Common Cause |
|---|---|---|
TypeError | Operation on wrong type | Calling method on undefined/null, passing wrong argument type |
ReferenceError | Variable not found | Typos, accessing before declaration, missing imports |
SyntaxError | Invalid code structure | Missing brackets, mismatched quotes, invalid JSON |
RangeError | Value outside allowed range | Infinite recursion (stack overflow), invalid array length |
URIError | Invalid URI operation | Malformed encodeURI() or decodeURI() input |
EvalError | Error in eval() | Rarely seen in modern code |
TypeError Examples
The most common error type. Happens when you access a property or call a method on undefined or null:
function getOrderTotal(order) {
return order.items.reduce((sum, item) => sum + item.price, 0);
// TypeError: Cannot read properties of undefined (reading 'reduce')
// → order.items is undefined
}
// Called with incomplete data:
getOrderTotal({ id: 123 }); // No 'items' propertyStack trace:
Uncaught TypeError: Cannot read properties of undefined (reading 'reduce')
at getOrderTotal (checkout.js:2:22)
at processCheckout (checkout.js:10:18)
at HTMLFormElement.<anonymous> (checkout.js:5:3)
The error is on line 2, but the root cause is on line 10 (or earlier) where getOrderTotal was called with an object missing the items property.
ReferenceError Examples
Happens when a variable name does not exist in the current scope:
function calculateTax(amount) {
const rate = getTaxRate(); // ReferenceError: getTaxRate is not defined
return amount * rate;
}Uncaught ReferenceError: getTaxRate is not defined
at calculateTax (tax.js:2:16)
at processPayment (payment.js:45:18)
Common causes: function was not imported, was misspelled, or is defined in a different module that was not loaded.
RangeError: Maximum Call Stack Size Exceeded
This happens with infinite recursion, when a function calls itself without a valid exit condition:
function flatten(arr) {
return flatten(arr.flat()); // Oops — no base case, calls itself forever
}
flatten([1, [2, [3]]]);Uncaught RangeError: Maximum call stack size exceeded
at flatten (utils.js:2:10)
at flatten (utils.js:2:10)
at flatten (utils.js:2:10)
... (hundreds of identical frames)
When you see the same function name repeating in every frame, you have infinite recursion. Check that function's termination condition.
Stack Traces in Different Environments
Chrome DevTools
Chrome shows the most detailed stack traces:
Uncaught TypeError: Cannot read properties of null (reading 'classList')
at toggleTheme (theme.js:15:23)
at HTMLButtonElement.handleClick (ui.js:42:5)
Chrome features:
- Clickable file links (jump to source in Sources panel)
- Async stack traces (shows across
awaitandsetTimeoutboundaries) - Source-mapped traces (shows original TypeScript/JSX, not compiled code)
Firefox Developer Tools
Firefox formats traces slightly differently:
TypeError: element is null
toggleTheme@theme.js:15:23
handleClick@ui.js:42:5
Firefox uses functionName@file:line:column instead of at functionName (file:line:column). The information is identical; only the format differs.
Node.js
Node.js includes the full file path:
TypeError: Cannot read properties of undefined (reading 'email')
at formatUser (/home/app/src/users/format.js:12:28)
at processUsers (/home/app/src/users/process.js:5:14)
at Object.<anonymous> (/home/app/src/index.js:3:1)
at Module._compile (node:internal/modules/cjs/loader:1376:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1435:10)
at Module.load (node:internal/modules/cjs/loader:1207:32)
at Module._resolveFilename (node:internal/modules/cjs/loader:1168:15)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:128:12)
The first few frames show your code. Frames starting with node:internal/ are Node.js internals; you can safely ignore them. The boundary between your code and Node internals is where you stop reading.
Stack Trace Format Comparison
| Feature | Chrome | Firefox | Node.js |
|---|---|---|---|
| Frame prefix | at | none (uses @) | at |
| Format | at fn (file:line:col) | fn@file:line:col | at fn (path:line:col) |
| Async traces | Yes | Yes | Yes (v12+) |
| Source maps | Yes (auto) | Yes (auto) | Yes (with flag) |
| Internal frames | Hidden by default | Hidden by default | Shown |
Reading Async Stack Traces
Modern JavaScript heavily uses async/await, Promises, and callbacks. When an error occurs inside async code, the synchronous call stack is short (often just the current function). Browser DevTools extend the trace to show the async chain:
async function loadUserSettings(userId) {
const user = await fetchUser(userId);
const settings = await fetchSettings(user.settingsId); // Error here
return settings;
}
async function initApp() {
const settings = await loadUserSettings(42);
renderApp(settings);
}
initApp();Chrome async stack trace:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'settingsId')
at loadUserSettings (app.js:3:49)
async
at initApp (app.js:8:22)
async
at app.js:12:1
The async labels mark boundaries between async operations. Reading this trace from bottom to top: the code at line 12 called initApp(), which awaited loadUserSettings(), which failed on line 3 because user (returned from fetchUser) was undefined.
Enable Async Stack Traces
Chrome DevTools enables async stack traces by default. In Node.js, async stack traces are available from v12+. In older Node versions, you only see the synchronous portion of the stack.
Reading Source-Mapped Stack Traces
In production, JavaScript is often minified and bundled. A raw stack trace might look like:
TypeError: a is not a function
at e (bundle.js:1:4523)
at t (bundle.js:1:2189)
This is unreadable. Source maps solve this by mapping bundled code back to the original files:
TypeError: processOrder is not a function
at validateCart (src/checkout/validation.ts:47:12)
at handleSubmit (src/checkout/form.ts:23:5)
Source maps are generated by build tools (Webpack, Vite, Rollup, esbuild) and loaded automatically by DevTools. In production, you can upload source maps to error tracking services like Sentry to get readable traces without exposing source maps publicly.
Practical Stack Trace Debugging Workflow
Here is a systematic approach to debugging any stack trace:
// Example error to debug:
// Uncaught TypeError: items.filter is not a function
// at filterActiveUsers (users.js:8:20)
// at updateUserList (dashboard.js:22:18)
// at fetchAndDisplay (dashboard.js:14:3)Read the error message
items.filter is not a function tells you that items exists but is not an array (or does not have a .filter method). It might be an object, string, number, or null.
Check the top frame
Open users.js line 8. Find the .filter() call. What variable is it called on? Add a breakpoint on the line before it and check the variable's actual type.
Check the calling frame
Open dashboard.js line 22. What is being passed to filterActiveUsers? The argument might be the wrong shape. This is often where the root cause lives.
Verify the data source
Open dashboard.js line 14. This is where the data originates (likely a fetch call). Check the API response in the Network panel. Is the response an array, or is it wrapped in an object like { data: [...] }?
In this example, the fix is likely on line 14 or 22: the API response needs to be unwrapped (response.data.users instead of response.data), or a default value needs to be provided (items || []).
Generating Stack Traces Programmatically
Using Error Objects
You can capture a stack trace at any point by creating an Error object:
function logCallPath(label) {
const trace = new Error(label);
console.log(trace.stack);
}
function processItem(item) {
logCallPath('Processing item');
// ... process the item
}
function processCart(cart) {
cart.items.forEach(processItem);
}Using console.trace()
A simpler way to log the current call path without creating an error:
function processPayment(amount) {
console.trace('Processing payment of $' + amount);
// Logs a stack trace showing how we got here
}Capturing Clean Stack Traces in Custom Errors
When building custom error classes, use Error.captureStackTrace (V8 engines) to exclude the error constructor from the trace:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
// Remove the constructor frame from the stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ValidationError);
}
}
}
function validateAge(age) {
if (age < 0 || age > 150) {
throw new ValidationError('age', `Invalid age: ${age}`);
}
}The stack trace starts at validateAge, not inside the ValidationError constructor, making it cleaner to read.
Best Practices
Stack Trace Reading Strategy
Follow these practices to debug faster using stack traces.
Always read the error message first. The message tells you what went wrong. The stack trace tells you where. Start with "what" and then look at "where."
Scan for your code in the frames. Ignore frames from libraries, frameworks, and runtime internals. Focus on frames that reference your own files. The bug is almost always in your code, not the library.
Check the second frame, not just the first. The top frame is where the error manifested, but the root cause is often in the caller (second or third frame). A TypeError in a utility function usually means the caller passed bad data.
Use source maps in production. Without source maps, stack traces from minified code are useless. Configure your build tool to generate source maps and upload them to your error tracking service.
Preserve stack traces when rethrowing errors. If you catch and rethrow, wrap the original error to preserve its stack:
try {
await processData(input);
} catch (originalError) {
// Preserves the original stack trace as the 'cause'
throw new Error('Data processing failed', { cause: originalError });
}Common Mistakes and How to Avoid Them
Stack Trace Misconceptions
These misunderstandings lead developers to look in the wrong place.
Assuming the error is always in the top frame. The top frame is where the symptom appears. The root cause is often 2-3 frames down, where incorrect data was created or passed. Always check the full chain.
Ignoring (anonymous) frames. Anonymous functions (callbacks, arrow functions, event handlers) appear as <anonymous> in stack traces. This makes debugging harder. Name your functions, even inline ones:
// Bad — shows as <anonymous> in stack traces
element.addEventListener('click', (e) => { /* ... */ });
// Better — shows as handleClick in stack traces
element.addEventListener('click', function handleClick(e) { /* ... */ });Swallowing errors in catch blocks. An empty catch block hides the stack trace entirely. Always log or rethrow:
// Bad — error disappears
try { riskyOperation(); } catch (e) {}
// Good — error is logged with its stack trace
try {
riskyOperation();
} catch (e) {
console.error('Operation failed:', e);
// Or rethrow: throw e;
}Not using Error.cause for wrapped errors. When you catch and rethrow, passing { cause: originalError } preserves the original stack trace. Without it, you lose the chain.
Stack Trace Cheat Sheet
| Stack Trace Element | Meaning |
|---|---|
Error type (e.g., TypeError) | What kind of error occurred |
| Error message | Specific description of the failure |
| Top frame | Where the error physically happened |
| Second frame | Who called the function that errored |
| Bottom frame | The program entry point |
<anonymous> | An unnamed function (callback/arrow) |
async label | An async boundary (await/Promise) |
node:internal/* | Node.js runtime internals (usually ignorable) |
(native) | Browser-internal code (ignorable) |
Next Steps
Practice reading stack traces
Intentionally write code that throws errors (access a property on undefined, call a non-function, trigger infinite recursion), read the stack trace, and identify the root cause. This builds fluency.
Set up source maps in your build
If you use a bundler, ensure source maps are generated for development and uploaded to your error tracking service for production. This makes every stack trace readable.
Implement structured error handling
Create custom Error subclasses for your application's domain (e.g., ValidationError, APIError, AuthenticationError) with clean stack traces and the cause property.
Install an error tracking service
Set up Sentry or a similar service to capture stack traces from production users. You get the full trace, browser info, user actions, and breadcrumbs without asking the user to open DevTools.
Rune AI
Key Insights
- Read top-to-bottom: the first frame is the error location, subsequent frames show the call path, and the last frame is the entry point
- Error type narrows the cause:
TypeErrormeans wrong type or missing property,ReferenceErrormeans missing variable,RangeErroroften means infinite recursion - Root cause is rarely in the top frame: check the second and third frames where data is constructed and passed to the failing function
- Source maps are essential for production: without them, minified stack traces are unreadable; configure your build tools to generate them
- Name your functions: anonymous callbacks show as
<anonymous>in stack traces, making debugging harder; named function expressions solve this
Frequently Asked Questions
Why does my stack trace show the wrong line numbers?
How do I get stack traces for unhandled Promise rejections?
Can I get a stack trace without throwing an error?
How long can a stack trace be?
Do stack traces work inside try-catch blocks?
What does "Uncaught" mean in a stack trace?
Conclusion
JavaScript stack traces provide a complete roadmap from where an error occurred to how the code arrived there. Reading the error message tells you what went wrong; reading the frames from top to bottom tells you which function called which, with exact file names and line numbers. The key insight is that the top frame shows the symptom, but the root cause often lives two or three frames deeper, where incorrect data was created or passed along.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.