Free Handbook · Runs in your browser

Errors & Exceptions

The built-in error types and what each one means, try / catch / finally, throwing and designing your own error classes, reading a stack trace top-down, and the rules for errors in async code and at the edges of a program.

0 / 111 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Name the six built-in error types and recognise each from its message
  • Use try / catch / finally without swallowing errors
  • Throw Error objects (never strings) and write custom error classes with a cause
  • Read a stack trace: the first line is what, the first at-line is where
  • Handle errors in promises and async functions correctly
01

The built-in error types

An exception is an error thrown at runtime that unwinds the call stack until something catches it — or nothing does, and the program (or the page) reports "Uncaught". JavaScript has one base Error and a handful of subclasses. The type tells you the category of mistake; the message tells you the specifics. Recognise them and most bugs diagnose themselves.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
ReferenceError: undefinedVariable is not defined
   instanceof Error: true | constructor: ReferenceError
TypeError: Cannot read properties of null (reading 'property')
   instanceof Error: true | constructor: TypeError
RangeError: toFixed() digits argument must be between 0 and 100
   instanceof Error: true | constructor: RangeError
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
   instanceof Error: true | constructor: SyntaxError
URIError: URI malformed
   instanceof Error: true | constructor: URIError
Error: custom
   instanceof Error: true | constructor: Error
SyntaxErrors in your own source stop the file from loading at all — they never reach a try/catch. The one you can catch is from JSON.parse.
TypeMeansTypical cause
ReferenceErrorA name does not existTypo, wrong scope, using a let before its line
TypeErrorA value is the wrong type for what you didReading a property of undefined/null, calling a non-function, reassigning a const
SyntaxErrorCode (or JSON) could not be parsedMissing bracket, import in a non-module, bad JSON
RangeErrorA number is out of rangeInfinite recursion (call stack), new Array(-1), toFixed(200)
URIErrorBad percent-encodingdecodeURIComponent("%")
AggregateErrorSeveral errors at oncePromise.any when everything rejects
02

try / catch / finally

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
  (checked "36")
36
  (checked "200")
null
  (checked "abc")
caller got: TypeError — "abc" is not a number
{ ok: true } null
cleanup
result
Your turn
Write divide(a, b) that throws a RangeError for b === 0, and a caller that catches only RangeError and lets anything else propagate.
Error you will hit

The empty catch: swallowing errors

javascript
async function loadUser(id) {
  try {
    return await api.get(`/users/${id}`)
  } catch (e) {
    // TODO handle
  }
}
const user = await loadUser(42)
console.log(user.name)
Uncaught TypeError: Cannot read properties of undefined (reading 'name')
    at your code:9
Why the engine said that

The request failed, the catch swallowed the error, the function returned undefined, and the crash happened three lines later with no clue why. Every hour spent debugging "undefined" starts with a silent catch somewhere.

The fix

Catch only what you can handle, and do something: return a sentinel and document it, log with context, or wrap and re-throw. Never leave a catch block empty.

javascript
async function loadUser(id) {
  try {
    return await api.get(`/users/${id}`)
  } catch (e) {
    throw new Error(`loadUser(${id}) failed`, { cause: e })
  }
}
Where to catch
As high as makes sense, as low as necessary. A helper should throw; the request handler, the click handler or main() should catch, log once with context, and show the user something. Catching in every function produces logs full of the same error five times and code that cannot tell success from failure.
03

throw and custom error classes

You can throw any value, and you should only ever throw Error objects: they carry a stack trace, a name and a message, and every tool expects them. When callers need to distinguish failures — not found vs. not allowed vs. invalid — make a class per kind and check with instanceof. Since ES2022, { cause } lets you wrap a low-level error inside a high-level one without losing it.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
ok [email protected]
404 user 2 not found
400 id → id: must be an integer
AppError: config.json is unreadable | cause: SyntaxError
string false undefined
Your turn
Add a ForbiddenError with status 403 and a toJSON() method on AppError that returns { error: name, message, status } — the shape an API would send.
err.name / err.message / err.stack
The three properties every Error has. stack is a string with the message and one line per frame.
cause
new Error("high level", { cause: lowLevelError }). Loggers print the chain. Use it whenever you catch and re-throw.
Error boundary
The place errors stop: an Express error middleware, a React ErrorBoundary, process.on("uncaughtException") as the last resort. Log there, once.
04

Reading a stack trace

A stack trace reads top-down: the first line is the error, the first at line is where it was thrown, and each line below is the caller of the one above. Find the first frame that is in your code (not node_modules, not node:internal) — that is where to look. The numbers are file:line:column, and editors make them clickable.

text
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
    at formatName (/app/src/users/format.js:12:23)      ← thrown here: line 12, col 23
    at /app/src/users/list.js:8:31                       ← called from an arrow in list.js
    at Array.map (<anonymous>)                           ← inside the built-in map
    at listUsers (/app/src/users/list.js:8:18)           ← which listUsers called
    at handler (/app/src/routes/users.js:22:20)          ← from the route handler
    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)
    at next (/app/node_modules/express/lib/router/route.js:149:13)

formatName at line 12 read .toUpperCase of something undefined — so a user in the list has no name. The bug is the data reaching line 12, and the fix is either validation upstream or a guard in formatName.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
TypeError: Cannot read properties of undefined (reading 'toUpperCase')
frames: present — top one is the throw site
message only: Cannot read properties of null (reading 'x')
full: TypeError | has stack: true
  1. 1
    Read the first line twice

    The type says the category; the message names the property or function. "reading 'x'" means the thing before .x was undefined.

  2. 2
    Find the first frame in your code

    Skip node_modules and node:internal. Open that file at that line.

  3. 3
    Ask "how did that value get here?"

    Walk down the frames — they are the call path. The bug is often two frames below the throw.

  4. 4
    Reproduce with the smallest input

    Turn the failing data into a test. Then fix. Then the test stays.

05

Errors in async code

A thrown error in a callback cannot be caught by a try around the code that scheduled it — that code finished long ago. Promises fix this by turning throws into rejections that travel down the chain; async/await makes them catchable with ordinary try/catch again. The rules: inside async functions, try/catch works; with .then chains, end with .catch; and every promise must have an owner.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
chain caught: step 2 failed
caught inside the callback: in timer
await caught: load failed
only a handler on the promise itself sees: load failed
all: b failed
allSettled: [ 'fulfilled', 'rejected' ]

Without the p.catch line, example 4 is an unhandled rejection: the try/catch around a call you did not await never sees the error.

The last-resort handlers
Browser: window.addEventListener("unhandledrejection", e => …) and window.onerror. Node: process.on("unhandledRejection") and process.on("uncaughtException") — log and exit, because the process is in an unknown state. Error-reporting services (Sentry) hook exactly these.
Mid-levelWhy does a try/catch around setTimeout not catch an error thrown in the callback?

The try block finishes as soon as setTimeout returns — it only schedules the callback. When the timer fires later, the callback runs from the event loop with a fresh, empty stack; the try frame no longer exists. To handle it, catch inside the callback, or wrap the timer in a promise and await it so the rejection flows to your try/catch.

What they are really testing: Whether you understand that the stack unwinds between the scheduling and the execution.

Finish the JavaScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.