JavaScript - Error Handling

Overview

Estimated time: 30–40 minutes

Learn practical error handling: throwing and catching errors, adding context with cause, defining custom error types, handling async errors with promises and async/await, and using aggregation helpers.

Learning Objectives

  • Use try/catch/finally and throw effectively.
  • Create custom error classes and attach context via cause.
  • Handle async errors in promise chains and async/await.
  • Apply Promise.allSettled and understand AggregateError from Promise.any.

Prerequisites

try/catch/finally and throw

function parseJSON(s) {
  try {
    return JSON.parse(s);
  } catch (e) {
    // Add context, rethrow
    throw new Error('Invalid JSON input', { cause: e });
  } finally {
    // Optional cleanup
  }
}

// parseJSON('not json'); // throws Error with cause

Custom error classes

class ValidationError extends Error {
  constructor(message, { field, cause } = {}) {
    super(message, { cause });
    this.name = 'ValidationError';
    this.field = field;
  }
}

function requirePositive(n) {
  if (!(Number.isFinite(n) && n > 0)) {
    throw new ValidationError('Expected positive number', { field: 'n' });
  }
  return n;
}

Async errors: promises and async/await

// Promise chain: use .catch at the end
const delay = (ms, v, fail=false) => new Promise((res, rej) => setTimeout(() => fail ? rej(new Error('boom')) : res(v), ms));

delay(20, 1)
  .then(x => x * 2)
  .then(x => { if (x > 1) throw new Error('too big'); return x; })
  .catch(err => { console.log('caught:', err.message); return 0; })
  .then(v => console.log('recovered:', v));

// async/await with try/catch
async function run() {
  try {
    const a = await delay(10, 'A');
    const b = await delay(10, 'B', true); // will reject
    return a + b;
  } catch (e) {
    return 'fallback';
  }
}

Aggregation helpers

// Collect results regardless of failures
const tasks = [delay(10, 'A'), delay(5, null, true), delay(8, 'C')];
const settled = await Promise.allSettled(tasks);
// settled: [{status:'fulfilled', value:'A'}, {status:'rejected', reason:Error}, {status:'fulfilled', value:'C'}]

// any: first fulfilled, otherwise throws AggregateError
try {
  const v = await Promise.any([delay(5, null, true), delay(6, 'OK')]);
  // v = 'OK'
} catch (e) {
  if (e instanceof AggregateError) {
    console.log('all failed:', e.errors);
  }
}

Best practices

  • Catch only where you can handle or add meaningful context; otherwise, rethrow.
  • Wrap external boundaries (I/O, parsing) to attach user-friendly messages and cause.
  • Prefer allSettled when partial success is acceptable; prefer any when one success suffices.

Common Pitfalls

  • Swallowing errors: Returning from catch hides failures; rethrow or log appropriately.
  • Unhandled rejections: Always terminate promise chains with .catch or wrap in try/catch for await.
  • Losing stack/context: Use new Error(message, { cause }) to preserve the underlying reason.
  • Finally assumptions: finally runs regardless of success/failure—avoid relying on returned values inside finally.

Try it

Run to see custom errors, cause, async try/catch, and aggregation: