Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Beginner/B16
16 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSB16 · Part 1 · Beginner

Errors & tools

The last lesson before the beginner project — reading what the engine is trying to tell you.

6 min read8 sections2 exercises
16 of 41

In this chapter

  • try / catch / finally
  • The built-in error types
  • finally can override the return value — the trap
  • debugger — a breakpoint you can commit
  • Custom errors

and 3 more

about 6 min
08On this page
  1. try / catch / finally
  2. The built-in error types
  3. finally can override the return value — the trap
  4. debugger — a breakpoint you can commit
  5. Custom errors
  6. Reading a stack trace
  7. console — more than .log
  8. DevTools, the short version

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

OrderWhat runsWhy
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

TypeThrown when
TypeErroran operation on the wrong type — calling a non-function, reading a property of undefined
RangeErrora number outside what's allowed — an invalid array length, (1).toFixed(101), recursion too deep ("Maximum call stack size exceeded")
ReferenceErrora name that doesn't exist in scope, or one still in the TDZ
SyntaxErrormalformed code — JSON.parse of invalid JSON, or a broken eval string
AggregateErrormore 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/finally inside a try/catch/finally, in the exact order each block actually runs.
  • Explain why a return inside finally silently discards the value from try, and why that's usually a bug.
  • Write a custom error class that extends Error and passes both instanceof Error and instanceof its 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 console methods besides .log and what each is for.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 10 testsParse JSON without crashingbeginner
  • 9 testsThrow a real, typed errorbeginner
All 35 for this level →
B16

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. B15Previous · B15Callbacks, then promises
  2. B16you are here
  3. Next · B17Guided project: build a to-do appEverything from the beginner chapters, in one small thing that works.5 min read

01On this page

8 sections · 0%

  1. 01try / catch / finally
  2. 02The built-in error types
  3. 03finally can override the return value — the trap
  4. 04debugger — a breakpoint you can commit
  5. 05Custom errors
  6. 06Reading a stack trace
  7. 07console — more than .log
  8. 08DevTools, the short version

[] chapters · t top