B16 · Part 1 · Beginner
Errors & tools
The last lesson before the beginner project — reading what the engine is trying to tell you.
08On this page
try / catch / finally
try {
JSON.parse("this isn't JSON"); // throws a SyntaxError
} catch (error) {
console.log("caught:", error.message);
} finally {
console.log("finally always runs — success, failure, doesn't matter");
}
try {
throw new Error("inner");
} finally {
console.log("finally ran");
}
Click run — you'll see "finally ran", and then the
error still shows up as uncaught below it. There's no
catch here at all, and finally doesn't stop
the error from propagating — it just guarantees that cleanup code
(closing a connection, hiding a spinner) runs on the way out, whether
the block succeeded or not.
The caught value doesn't have to be named if you don't need it — useful when you only care that something failed:
try {
riskyThing();
} catch { // no (error) — the binding is optional since ES2019
showFallbackUI();
}
function run() {
try {
try {
throw new Error("inner");
} finally {
console.log("inner finally");
}
} catch (e) {
console.log("outer catch:", e.message);
} finally {
console.log("outer finally");
}
}
run();
Dry run: which block runs, in what order
| Order | What runs | Why |
|---|---|---|
| 1st | "inner finally" | the inner try has no catch, so the error isn't handled here — but finally still runs on the way out, before the error keeps propagating |
| 2nd | "outer catch: inner" | the error, still unhandled, reaches the outer try's catch — this is the first block that actually stops it |
| 3rd | "outer finally" | runs last, after the outer catch has already handled the error — finally always runs, caught or not |
A finally without a matching catch doesn't
stop an error — it only guarantees its own cleanup code runs before the
error keeps climbing to look for a catch further out.
The built-in error types
| Type | Thrown when |
|---|---|
TypeError | an operation on the wrong type — calling a non-function, reading a property of undefined |
RangeError | a number outside what's allowed — an invalid array length, (1).toFixed(101), recursion too deep ("Maximum call stack size exceeded") |
ReferenceError | a name that doesn't exist in scope, or one still in the TDZ |
SyntaxError | malformed code — JSON.parse of invalid JSON, or a broken eval string |
AggregateError | more than one error at once — what Promise.any rejects with when every promise fails |
try {
null.name;
} catch (e) {
console.log(e instanceof TypeError, e.constructor.name); // true "TypeError"
}
try {
new Array(-1);
} catch (e) {
console.log(e instanceof RangeError); // true
}
Every one of these extends Error — same
.message/.stack shape — so a
catch (e) can always fall back to treating it generically.
But checking e instanceof TypeError specifically lets
code react differently to "you passed the wrong shape of data" versus
"the network is down" versus "you wrote invalid JSON," instead of
pattern-matching the message string, which can change between engine
versions.
finally can override the return value — the trap
function attempt() {
try {
return "try";
} finally {
return "finally"; // ← this WINS, silently discarding "try"
}
}
console.log(attempt()); // what happens?
"finally" — a return (or throw,
or break/continue) inside
finally doesn't run alongside the try's
outcome, it replaces it completely, even swallowing an error
that was already thrown. This is almost always a bug rather than a
choice — a real finally block should stick to cleanup
(closing a connection, clearing a timer) and never contain its own
return.
debugger — a breakpoint you can commit
function processOrder(order) {
debugger; // execution pauses HERE the instant DevTools is open
const total = order.items.reduce((sum, i) => sum + i.price, 0);
return total;
}
With DevTools closed, debugger; does nothing at all —
not an error, not even a warning. With DevTools open, it pauses
exactly like clicking a line number in the Sources panel, every local
variable inspectable in the scope pane. It's useful specifically
because it travels with the code: a click-added breakpoint disappears
on refresh, but a debugger; statement fires every time
that line runs until it's deleted — handy for a rare, hard-to-reproduce
path, and worth grepping for before shipping.
Custom errors
Error is a class like any other — extend it to attach
your own data, and instanceof still recognizes the whole
chain.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
try {
throw new ValidationError("age must be positive", "age");
} catch (e) {
console.log(e.name, "-", e.message, "- field:", e.field);
console.log("is an Error:", e instanceof Error);
console.log("is a ValidationError:", e instanceof ValidationError);
}
Both instanceof checks come back true —
super(message) wires up the normal Error
machinery (.message, .stack), and the
class ... extends Error keeps the prototype chain intact.
That lets calling code catch broadly (instanceof Error)
or specifically (instanceof ValidationError) depending on
what it actually needs to handle differently.
Reading a stack trace
Every Error carries a .stack string — a
snapshot of every function call that was still active the moment it
was thrown, most-recent first:
Error: Cannot read properties of undefined (reading 'name')
at getDisplayName (utils.js:12:18)
at renderUser (UserCard.js:8:24)
at renderApp (App.js:22:3)
at main (index.js:5:1)
Read it top to bottom, most specific first: line 1 is where
the error actually happened — inside getDisplayName, at
utils.js line 12. Every line under it is a caller, in
order, all the way out to where the whole chain started. The bug is
almost always at or near the top; the rest of the trace is just
"how did we get here."
⚠ The throw site isn't always the bug
A
TypeError reading a property of undefined
tells you where it blew up, not where it went wrong.
The real bug is usually a few frames up — whatever handed
getDisplayName an object it shouldn't have. Read the
whole trace before fixing the top line.
console — more than .log
| Call | For |
|---|---|
console.log(...) | general output |
console.info(...), console.debug(...) | same as log, different icon — some filters hide/show them separately |
console.warn(...) | yellow, doesn't stop anything — a heads-up |
console.error(...) | red, includes a stack trace automatically |
console.table(data) | an array of objects, rendered as an actual table |
console.group(label) / .groupEnd() | indents everything between them — collapsible in DevTools |
console.time(label) / .timeEnd(label) | how long the code between them took |
console.table([
{ name: "Ana", age: 29, role: "admin" },
{ name: "Ravi", age: 34, role: "editor" },
]);
Open your own DevTools console and run that — every object becomes a row, every shared key becomes a column, automatically. It's the single fastest way to eyeball an array of records without writing a loop just to look at it.
DevTools, the short version
- Elements panel — the live DOM tree, editable in place. Change a class or a style here to test an idea before touching the file.
- Console panel — everything above, plus a REPL you can run arbitrary code in, against the actual page that's open.
-
Sources panel — set a real breakpoint by clicking a line
number, or drop
debugger;directly in your code. Either one pauses execution right there, with every variable in scope inspectable. - Network panel — every request the page made, its status, timing, and response — the first place to look when "the data never showed up."
Say it like this → "console.log tells you
what you thought to ask for. A breakpoint lets you stop time and
inspect everything — including the things you didn't think to log."
Interview answer, one sentence
"try/catch handles synchronous throws and awaited rejections, finally always runs and can even override a return value, and a good custom error extends Error, sets its name and keeps its cause — while a stack trace lists the throw site first, with the callers beneath it."
Before you move on
- Trace a nested
try/finallyinside atry/catch/finally, in the exact order each block actually runs. - Explain why a
returninsidefinallysilently discards the value fromtry, and why that's usually a bug. - Write a custom error class that extends
Errorand passes bothinstanceof Errorandinstanceofits own class. - Read a stack trace and say which line is the actual bug versus which lines are just "how we got here."
- Name at least three
consolemethods besides.logand what each is for.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series