Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 14

Error Handling

This chapter assumes you are comfortable with functions, control flow, and the difference between synchronous and asynchronous code. You should understand that JavaScript has a single call stack and that promise rejection is the asynchronous analogue of a thrown exception. Familiarity with `class` s
Level: AdvancedLength: In depth4 min read

Read at another depth

Error Handling

Prerequisites

This chapter assumes you are comfortable with functions, control flow, and the difference between synchronous and asynchronous code. You should understand that JavaScript has a single call stack and that promise rejection is the asynchronous analogue of a thrown exception. Familiarity with class syntax is needed for the custom error section.

The shape of a failure

JavaScript failures fall into three categories, and each needs a different response:

  1. Runtime errors — thrown by the engine (TypeError, RangeError, SyntaxError, ReferenceError) or by your code via throw.
  2. Promise rejections — an exception that escaped an async function or was passed to Promise.reject. They travel through the promise chain rather than the call stack.
  3. Unexpected termination — uncaughtException / unhandledrejection events, or a process-level crash. These are last-resort signals, not handling mechanisms.

The engine throws TypeError when you access a property of null or undefined, RangeError when a value is out of a valid domain, and ReferenceError when an identifier cannot be resolved. Errors thrown by user code are conventionally instances of Error or a subclass.

throw, try, catch, finally

Code example in javascript
function parsePort(raw) {
  const port = Number(raw);
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
    throw new RangeError(`invalid port: ${JSON.stringify(raw)}`);
  }
  return port;
}

try {
  const port = parsePort(process.argv[2] ?? '8080');
  console.log('using port', port);
} catch (err) {
  console.error('startup failed:', err.message);
  process.exitCode = 1;
} finally {
  console.log('parse attempt finished');
}

throw can technically throw any value, but never do: throw 'string' produces a rejection with no stack and no .name. Always throw Error instances. finally runs whether the block exits normally, via a return, or via a thrown error — which makes it the wrong place to swallow errors but the right place to release resources (close handles, clear timers). Note that a return inside finally silently discards a pending exception; treat that as a bug.

Rethrowing and the stack

err.stack is captured when the Error is constructed, not when thrown. Two common gotchas:

  • Calling throw err in a catch preserves the original stack — good. Creating a fresh new Error(...) discards it. To add context while preserving causality, use cause:
Code example in javascript
try {
  await fetch('https://api.example.com/v1/users');
} catch (err) {
  throw new Error('failed to load users', { cause: err });
}
  • finally blocks execute while the stack unwinds, so a return in finally overwrites both the return value and any pending throw.

Custom error types

Subclassing Error gives you instanceof-based dispatch, which is far more robust than string matching on message:

Code example in javascript
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

class NetworkError extends Error {
  constructor(message, status) {
    super(message);
    this.name = 'NetworkError';
    this.status = status;
  }
}

async function getUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  if (res.status === 404) {
    throw new NetworkError('user not found', 404);
  }
  if (!res.ok) {
    throw new NetworkError(`status ${res.status}`, res.status);
  }
  return res.json();
}

try {
  const user = await getUser(42);
  console.log(user);
} catch (err) {
  if (err instanceof NetworkError && err.status === 404) {
    console.warn('no such user; showing empty state');
  } else {
    throw err; // not ours to handle — propagate
  }
}

One subtlety: when targeting ES5 transpilation, subclasses of Error historically lost the proper prototype and stack behavior. Modern engines and ES2022 extends Error are fine, but if your code passes through old Babel targets, instanceof checks can fail. Verify with a test.

Asynchronous errors

Inside an async function, a thrown error becomes a rejected promise automatically:

Code example in javascript
async function main() {
  try {
    await getUser(1);
  } catch (err) {
    console.error('caught via try/catch:', err.message);
  }
}

The dangerous cases are the ones that escape:

  • A promise with no catch and no await — an unhandled rejection. In Node, this crashes the process by default; in browsers it is logged and surfaces via window.onunhandledrejection.
  • Promise.all rejects fast on the first failure but leaves other promises running; failures you did not observe are unhandled. Use allSettled when you need every outcome, or attach a .catch(() => {}) to fire-and-forget work deliberately.
  • Synchronous throw inside a plain (non-async) callback passed to an async API cannot be caught by the caller's try/catch; it must be converted to a rejection inside the API.

Catching at boundaries, not everywhere

An advanced codebase handles errors at architectural boundaries and lets them propagate through the middle:

  • Validate and throw at input edges (parsing, API handlers).
  • Propagate through internal layers untouched — do not catch (e) { console.log(e) } and continue; that converts a crash into silent corruption.
  • Translate only at boundaries where a different representation is needed (HTTP status, user-facing message), attaching the original as cause.
  • Log once. Wrapping and re-logging at every layer produces duplicate stacks and obscures the origin.

For global safety nets, register process.on('unhandledRejection') (Node) or window.addEventListener('unhandledrejection', ...) (browser) — but treat a hit on those handlers as a bug in coverage, not an expected path.

Exercise

Why is throw new Error('x', { cause: err }) preferred over throw err after logging, and over throwing a brand-new error with the same message?

Show answer

Throwing the original error loses the high-level context added at this layer; throwing a fresh error with the same message discards the original stack entirely. Wrapping with cause preserves the full original stack and message while adding the new context, so a top-level handler can print the whole chain.

Exercise

A handler does saveToDb(record); res.json({ ok: true }); where saveToDb is async and returns a promise that rejects on a constraint violation. Why does the response still say ok: true, and how do you fix it?

Show answer

The promise is created but never awaited or given a .catch, so the rejection is unhandled and the synchronous code continues immediately. Fix by await-ing the promise inside a try/catch (or attaching .catch if it is intentionally fire-and-forget) and returning an error response on failure.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Error Handling

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that