Skip to main content

Errors and error handling

Error handling in JavaScript works on three levels: a local try/catch/finally around risky code, rejection handling for asynchronous operations, and global handlers as the last line of defence. An error is raised with the throw operator, and for domain scenarios you describe custom classes that extend Error.

Theory

TL;DR

  • try/catch/finally catches synchronous exceptions; finally always runs.
  • An error is raised with throw new Error('...'), and you should throw an Error object, not a string.
  • Custom classes (class ValidationError extends Error) give typed domain errors; { cause } preserves the original reason.
  • Plain try/catch works with async/await; promise chains use .catch().
  • Promise.allSettled keeps one failed operation from killing the whole batch.
  • Global handlers: window.addEventListener('error' | 'unhandledrejection') in the browser, process.on('uncaughtException' | 'unhandledRejection') in Node.js.

Quick example

javascript
try { risky(); // code that may fail } catch (err) { console.error(err.message); // handling or logging } finally { cleanup(); // always runs (even on an error) }

Raising an error:

javascript
if (!user) { throw new Error('User not found'); }

Custom error classes

Extending Error lets you tell domain errors apart from technical ones and handle them selectively with instanceof.

javascript
class ValidationError extends Error { constructor(message, cause) { super(message, { cause }); // Node 16+ and modern browsers: err.cause this.name = 'ValidationError'; } } throw new ValidationError('Invalid email');

The cause field keeps the original error, so the low-level reason is not lost when you wrap it into something the user can understand.

Asynchronous code

a) async/await with try/catch

javascript
async function load() { try { const res = await fetch('/api/data'); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { // handling, retry or notification console.error('Load failed:', err); return null; // or rethrow it: throw err } }

Note that fetch does not throw on an HTTP status of 404 or 500, so the status is checked by hand through res.ok.

b) Promises: .catch()

javascript
fetch('/api/data') .then(r => r.json()) .catch(err => console.error('Request failed', err));

c) Parallel operations without failing the whole batch

javascript
const results = await Promise.allSettled(urls.map(u => fetch(u))); for (const r of results) { if (r.status === 'fulfilled') console.log('OK', r.value); else console.warn('Failed', r.reason); }

Promise.all rejects on the very first failure, while Promise.allSettled waits for all of them and reports the status of each operation separately.

Global handlers (the last line of defence)

Browser

javascript
// synchronous and uncaught exceptions window.addEventListener('error', (event) => { console.error('Uncaught error:', event.error, event.message, event.filename, event.lineno); }); // uncaught promise rejections window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled rejection:', event.reason); });

Node.js

javascript
process.on('uncaughtException', (err) => { console.error('Uncaught Exception', err); // logging and a clean shutdown of the process process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled Rejection', reason); // the decision depends on the application policy: log and exit, or carry on });

Global handlers are a safety net, not a replacement for local handling.

After an uncaughtException the process is in an undefined state, so the recommended practice is to log, close resources cleanly and exit, leaving the restart to a process manager.

Common mistakes

  • An empty catch. A block that silently swallows the exception turns a bug into invisible behaviour; log it at the very least.
  • throw 'text' instead of throw new Error('text'). A string has neither stack nor name, so the stack trace is lost.
  • A missing await before a call inside try. The promise rejects after the block has already been left, and catch never sees it.
  • Expecting fetch to throw on a 500. It only rejects on a network failure; the status is checked with res.ok.
  • A try/catch around an asynchronous callback. An exception inside setTimeout(() => { ... }) escapes in a different event loop tick and the outer block does not catch it.
  • A global handler instead of a local one. It is fine for telemetry, but it gives the user no meaningful message and does not restore the application state.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.