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/finallycatches synchronous exceptions;finallyalways runs.- An error is raised with
throw new Error('...'), and you should throw anErrorobject, not a string. - Custom classes (
class ValidationError extends Error) give typed domain errors;{ cause }preserves the original reason. - Plain
try/catchworks withasync/await; promise chains use.catch(). Promise.allSettledkeeps 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
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:
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.
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
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()
fetch('/api/data')
.then(r => r.json())
.catch(err => console.error('Request failed', err));c) Parallel operations without failing the whole batch
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
// 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
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 ofthrow new Error('text'). A string has neitherstacknorname, so the stack trace is lost.- A missing
awaitbefore a call insidetry. The promise rejects after the block has already been left, andcatchnever sees it. - Expecting
fetchto throw on a 500. It only rejects on a network failure; the status is checked withres.ok. - A
try/catcharound an asynchronous callback. An exception insidesetTimeout(() => { ... })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 readyA concise answer to help you respond confidently on this topic during an interview.