A stack trace is a list of "who called whom", newest first. Most of us skim it for a file name we recognise. A few habits make the trace do that work for you.
Read it top down, stop at your code
function parseSeconds(text: string): number { const value = Number.parseFloat(text); if (Number.isNaN(value)) { throw new Error(`Not a duration: "${text}"`); } return value; } function totalSeconds(parts: string[]): number { return parts.map(parseSeconds).reduce((a, b) => a + b, 0); } totalSeconds(["1.5", "2", "soon"]); The top frame is where the error was thrown: parseSeconds. Below it, the frames that led there: the array method, then totalSeconds, then the top-level call. Read down until you hit the first frame in code you wrote. That's usually the line to fix, or the line that passed the bad value.
Throw Errors, not strings
throw "config missing"; // no stack, no line throw new Error("config missing"); // stack, message, line A string has no stack property. An Error captures the stack where it was created. Always throw (and reject with) Error objects.
Name your functions
parts.map(parseSeconds) puts parseSeconds in the trace. parts.map((t) => { … }) puts an anonymous frame there. When a function is worth debugging, it's worth a name.
Wrap with cause, don't swallow
When you catch and rethrow, keep the original:
function loadTimings(parts: string[]) { try { return totalSeconds(parts); } catch (err) { throw new Error("Could not load timings", { cause: err }); } } loadTimings(["1.5", "soon"]); Now the error says what you were doing ("Could not load timings") and still carries the low-level reason ("Not a duration") in err.cause, with its own stack. { cause } is standard since ES2022.
When the trace points at the wrong file
TypeScript, JSX and bundlers all run code that isn't byte-for-byte what you wrote, so line numbers can drift. Source maps fix that in browsers and in Node with --enable-source-maps; without them, you're debugging the compiled output.
The same trace in a scratchpad
I run snippets like these in JSLab, a JavaScript & TypeScript scratchpad for macOS, where the mapping is done for you:
- An error names the line it came from, underlines it in the editor and marks it in the margin.
- Its stack frames are mapped back to your source lines, TypeScript included, so they point at the code you wrote. Click a frame to jump to it.
- Frames from outside your code aren't listed; a note says how many internal frames were left out.
- An error's
causeis shown with it.
Here's a different snippet in a real JSLab TypeScript tab: tabWidth reads settings.editor.tabWidth on a config with no editor section. The error row is tagged :7, line 7 is underlined and marked in the margin, and the frames read tabWidth (L7:19) and <anonymous> (L10:1).
Checklist
- Read the trace top down; stop at the first frame you own.
- Throw
Errors. Name functions you'll want to find. - Rethrow with
{ cause }. - Make sure source maps are on wherever you compile.
JSLab launches Nov 15. Errors and stack frames: https://js-lab.app/docs/output
