JavaScript · Syntax · Advanced

Error Handling

10 tasks

Custom errors, try/catch/finally, async errors, and defensive patterns

Custom errors, try/catch/finally, and selective re-throw

#
**The Error hierarchy and custom errors** JavaScript has a built-in `Error` class and several specialisations: `TypeError`, `RangeError`, `SyntaxError`, etc. You can extend `Error` to create domain-specific errors that carry extra context: ```js class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; // override default 'Error' this.field = field; // extra context } } function validateAge(age) { if (typeof age !== 'number') throw new TypeError('age must be a number'); if (age < 0 || age > 150) throw new ValidationError('age out of range', 'age'); return true; } try { validateAge(-5); } catch (err) { if (err instanceof ValidationError) { console.log(err.name, err.field, err.message); // 'ValidationError', 'age', 'age out of range' } } ``` **try / catch / finally — execution order** `finally` always runs regardless of whether an error was thrown or caught: ```js function openResource() { const resource = { open: true }; try { if (Math.random() < 0.5) throw new Error('random failure'); return resource; // finally still runs before the return lands } catch (err) { console.error('caught:', err.message); return null; } finally { console.log('cleanup — always runs'); } } ``` **Selective re-throw — only catch what you can handle** A common mistake is catching every error silently. Only handle errors you know about; re-throw the rest: ```js try { riskyOperation(); } catch (err) { if (err instanceof ValidationError) { showUserMessage(err.message); } else { throw err; // unexpected — let it propagate } } ```

Async errors, Promise.allSettled, and safeParseJSON

#
**Async error handling — await inside try/catch** With `async/await`, rejected Promises surface as thrown errors. Wrap `await` calls in `try/catch` just like synchronous code: ```js async function fetchUser(id) { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error('fetchUser failed:', err.message); return null; // safe fallback } } ``` **Promise.allSettled — don't fail the whole batch** Unlike `Promise.all` (which rejects on the first failure), `Promise.allSettled` waits for all and gives you `{status, value/reason}` for each: ```js const results = await Promise.allSettled([ fetch('/api/a'), fetch('/api/b'), fetch('/api/c'), ]); for (const r of results) { if (r.status === 'fulfilled') { console.log('OK:', r.value); } else { console.warn('failed:', r.reason.message); } } ``` **safeParseJSON — graceful JSON parse** ```js function safeParseJSON(str, fallback = null) { try { return JSON.parse(str); } catch { return fallback; } } safeParseJSON('{"a":1}'); // {a: 1} safeParseJSON('bad json'); // null safeParseJSON('bad json', []); // [] ```

Result pattern, error handling reference, and common pitfalls

#
**The Result pattern — errors as values** Instead of throwing, return a discriminated object. This makes the error path explicit and forces callers to handle it: ```js function divide(a, b) { if (b === 0) return { ok: false, error: 'division by zero' }; return { ok: true, value: a / b }; } const result = divide(10, 0); if (!result.ok) { console.error(result.error); } else { console.log(result.value); } ``` **Error handling quick reference** | Scenario | Recommended approach | |---|---| | Expected business error | Custom `Error` subclass + `instanceof` check | | Unexpected / programmer error | Let it propagate (`throw`), don't swallow | | Async code | `await` inside `try/catch` | | Multiple async calls | `Promise.allSettled` for partial failure tolerance | | Parsing untrusted data | `try/catch` + fallback (`safeParseJSON` pattern) | | Library/API caller UX | Result pattern `{ ok, value/error }` | **Common pitfalls** ```js // BAD: swallowing errors silently try { riskyOp(); } catch (e) { /* nothing */ } // BAD: catching Error base class without re-throwing unknowns catch (err) { if (err instanceof Error) logIt(err); } // BAD: forgetting await — unhandled rejection! async function bad() { try { fetch('/api'); // no await — catch won't fire if this rejects } catch (e) { /* never fires */ } } // GOOD: always await inside try/catch async function good() { try { await fetch('/api'); } catch (e) { /* fires correctly */ } } ```
01

Implement a custom ValidationError class

#

Create a `ValidationError` class that extends `Error`. The constructor takes `message` and `field`. Set `this.name = "ValidationError"` so it distinguishes itself from plain errors. Verify that `instanceof ValidationError` and `instanceof Error` both return `true`.

class ValidationError extends Error {
  // add: name='ValidationError', field property
}

// Test:
try {
  throw new ValidationError('email is required', 'email');
} catch (err) {
  console.log(err.name);    // 'ValidationError'
  console.log(err.message); // 'email is required'
  console.log(err.field);   // 'email'
  console.log(err instanceof ValidationError); // true
  console.log(err instanceof Error);           // true
}
Solution
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

try {
  throw new ValidationError('email is required', 'email');
} catch (err) {
  console.log(err.name);    // 'ValidationError'
  console.log(err.message); // 'email is required'
  console.log(err.field);   // 'email'
  console.log(err instanceof ValidationError); // true
  console.log(err instanceof Error);           // true
}
02

Predict the try/catch/finally execution order

#

What is the exact output of this code? Explain why `finally` runs even though `catch` has a `return` statement, and why `"after throw"` is never logged.

function test() {
  try {
    console.log('try');
    throw new Error('oops');
    console.log('after throw');
  } catch (e) {
    console.log('catch:', e.message);
    return 'from catch';
  } finally {
    console.log('finally');
  }
}

const result = test();
console.log('result:', result);
Solution
// Output order:
// try
// catch: oops
// finally          ← runs before the return from catch lands
// result: from catch

// 'after throw' is never logged because execution jumps to catch
// finally always runs even when catch has a return statement
03

Handle async errors with try/catch

#

Implement `fetchData(url)` as an `async` function. Use `fetch` to make a request. If `res.ok` is false, throw an `Error` that includes the HTTP status code. Catch any error, log `"fetchData failed: <message>"`, and return `null`.

async function fetchData(url) {
  // fetch url, throw if !res.ok (include HTTP status in message)
  // on error: log the message and return null
}
Solution
async function fetchData(url) {
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error('fetchData failed:', err.message);
    return null;
  }
}
04

Fetch multiple URLs with Promise.allSettled

#

Implement `fetchAll(urls)` that fetches all URLs concurrently and returns `{ succeeded: [...responses], failed: [...errorMessages] }`. Some requests may fail — don't let one failure cancel the others. Use `Promise.allSettled`.

async function fetchAll(urls) {
  // fetch all urls concurrently
  // return { succeeded: [...responses], failed: [...errorMessages] }
}
Solution
async function fetchAll(urls) {
  const results = await Promise.allSettled(urls.map(u => fetch(u)));
  const succeeded = [];
  const failed = [];
  for (const r of results) {
    if (r.status === 'fulfilled') succeeded.push(r.value);
    else failed.push(r.reason.message);
  }
  return { succeeded, failed };
}
05

Selectively handle and re-throw errors

#

Complete `handleRequest()` to handle `NetworkError` (log `"retry later"`) and `AuthError` (log `"please log in"`) selectively. Any other error type must be re-thrown so it propagates to the caller.

class NetworkError extends Error {
  constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
class AuthError extends Error {
  constructor(msg) { super(msg); this.name = 'AuthError'; }
}

function handleRequest() {
  try {
    riskyOperation(); // may throw NetworkError, AuthError, or Error
  } catch (err) {
    // handle NetworkError: log 'retry later'
    // handle AuthError: log 'please log in'
    // anything else: re-throw
  }
}
Solution
class NetworkError extends Error {
  constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
class AuthError extends Error {
  constructor(msg) { super(msg); this.name = 'AuthError'; }
}

function handleRequest() {
  try {
    riskyOperation();
  } catch (err) {
    if (err instanceof NetworkError) {
      console.log('retry later');
    } else if (err instanceof AuthError) {
      console.log('please log in');
    } else {
      throw err; // unexpected — propagate
    }
  }
}
06

Implement safeParseJSON with a fallback

#

Implement `safeParseJSON(str, fallback = null)` that attempts to parse `str` as JSON. If parsing fails for any reason, return `fallback` instead of throwing. The function must never throw.

function safeParseJSON(str, fallback = null) {
  // parse JSON safely, return fallback on invalid input
}
Solution
function safeParseJSON(str, fallback = null) {
  try {
    return JSON.parse(str);
  } catch {
    return fallback;
  }
}

safeParseJSON('{"name":"Alice"}');       // {name: 'Alice'}
safeParseJSON('invalid json');           // null
safeParseJSON('bad', []);               // []
safeParseJSON('bad', { default: true }); // { default: true }
07

Collect multiple validation errors before throwing

#

Implement `AggregateError` (extends `Error`, stores `.errors` array) and `validateUser(data)`. Validate: `name` is non-empty string, `age` is number ≥ 0, `email` contains `"@"`. Collect ALL failures and throw a single `AggregateError` if any exist.

class AggregateError extends Error {
  constructor(errors, message) {
    // errors is an array of Error objects
    // store them on this.errors
  }
}

function validateUser(data) {
  const errors = [];
  // validate: data.name must be non-empty string
  // validate: data.age must be a number >= 0
  // validate: data.email must contain '@'
  // if any errors, throw AggregateError with message 'Validation failed'
}
Solution
class AggregateError extends Error {
  constructor(errors, message) {
    super(message);
    this.name = 'AggregateError';
    this.errors = errors;
  }
}

function validateUser(data) {
  const errors = [];
  if (!data.name || typeof data.name !== 'string')
    errors.push(new Error('name must be a non-empty string'));
  if (typeof data.age !== 'number' || data.age < 0)
    errors.push(new Error('age must be a non-negative number'));
  if (!data.email || !data.email.includes('@'))
    errors.push(new Error('email must contain @'));
  if (errors.length) throw new AggregateError(errors, 'Validation failed');
}

try {
  validateUser({ name: '', age: -1, email: 'bad' });
} catch (err) {
  console.log(err.message);  // 'Validation failed'
  err.errors.forEach(e => console.log(' -', e.message));
}
08

Implement the Result pattern for error handling

#

Implement `divide(a, b)` and `safeSqrt(n)` using the Result pattern: return `{ ok: true, value }` on success and `{ ok: false, error }` on failure. Neither function should throw. Division by zero and negative square root are error cases.

function divide(a, b) {
  // return { ok: true, value } or { ok: false, error }
  // do NOT throw
}

function safeSqrt(n) {
  // return { ok: true, value } or { ok: false, error }
  // n < 0 is an error
}
Solution
function divide(a, b) {
  if (b === 0) return { ok: false, error: 'division by zero' };
  return { ok: true, value: a / b };
}

function safeSqrt(n) {
  if (n < 0) return { ok: false, error: 'cannot take sqrt of negative number' };
  return { ok: true, value: Math.sqrt(n) };
}

// Usage:
const r1 = divide(10, 2);
if (r1.ok) console.log(r1.value); // 5

const r2 = divide(10, 0);
if (!r2.ok) console.log(r2.error); // 'division by zero'

const r3 = safeSqrt(-4);
if (!r3.ok) console.log(r3.error); // 'cannot take sqrt of negative number'
09

Chain errors with the cause option

#

Implement `loadConfig(path)` that reads a file with `readFile(path)` and parses it with `JSON.parse`. Catch any error and wrap it: `throw new Error("Failed to load config", { cause: err })`. This preserves the original error on `.cause`.

// Error cause chaining: new Error('msg', { cause: originalErr })
async function loadConfig(path) {
  // read file (simulate: may throw 'file not found' Error)
  // parse JSON (may throw SyntaxError)
  // wrap any error with cause and message 'Failed to load config'
}

// Simulate helpers:
async function readFile(path) {
  if (path === 'missing.json') throw new Error('file not found');
  return '{"valid": true}';
}
Solution
async function loadConfig(path) {
  try {
    const text = await readFile(path);
    return JSON.parse(text);
  } catch (err) {
    throw new Error('Failed to load config', { cause: err });
  }
}

async function readFile(path) {
  if (path === 'missing.json') throw new Error('file not found');
  return '{"valid": true}';
}

// Usage:
try {
  await loadConfig('missing.json');
} catch (err) {
  console.log(err.message);       // 'Failed to load config'
  console.log(err.cause.message); // 'file not found'
}
10

Set up global unhandled rejection handlers

#

Explain what an unhandled Promise rejection is and when it occurs. Write the global handler registration for both browser (`window`) and Node.js (`process`). The handler should log `"Unhandled rejection: <reason>"`. Demonstrate by creating a rejected Promise that has no `.catch()`.

// In a browser or Node.js environment:
// Set up a global handler for unhandled Promise rejections
// that logs: 'Unhandled rejection: <reason>'

// In browser:
// window.addEventListener('unhandledrejection', handler)

// In Node.js:
// process.on('unhandledRejection', handler)

// Demonstrate by creating a rejected promise with no .catch()
Solution
// Browser:
window.addEventListener('unhandledrejection', (event) => {
  console.log('Unhandled rejection:', event.reason);
  event.preventDefault(); // prevents browser console error
});

// Node.js:
process.on('unhandledRejection', (reason, promise) => {
  console.log('Unhandled rejection:', reason);
});

// Trigger it:
Promise.reject(new Error('forgot to catch me'));
// → 'Unhandled rejection: Error: forgot to catch me'

// Always prefer .catch() or try/catch over relying on this handler —
// this is a last resort to prevent silent failures.