JavaScript Error Handling
Handle errors with try/catch and finally, throw and create custom Error types, and catch async errors.
TL;DR
- Wrap risky code in
try/catchto handle thrown errors cleanly. - Throw custom error classes to make
catchblocks more precise. - Use
finallyto run cleanup code regardless of success or failure.
Try/Catch Basics
try/catchWrap risky code in a try block to intercept runtime errors.
try {
const result = riskyOperation();
console.log(result);
} catch (error) {
console.error('Error:', error.message);
}Catch only runs on errorThe catch block only runs when an error is thrown in try.
try {
const data = JSON.parse('invalid');
} catch (error) {
console.error('Caught:', error.message); // SyntaxError
}Error object propertiesThe error object contains a message, name, and a stack trace.
catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // "Unexpected token i"
console.log(error.stack); // full trace
}Finally Block
Always runsRun cleanup code with finally, which always executes.
try {
const file = openFile('data.txt');
processFile(file);
} catch (error) {
console.error('Error:', error);
} finally {
closeFile(); // Always runs
}Runs before returnFinally runs even if catch re-throws or the try block returns early.
function getData() {
try {
return fetchData();
} finally {
cleanup(); // runs before function returns
}
}Resetting UI stateFinally is useful for resetting loading or spinner state in UIs.
setLoading(true);
try {
await fetchData();
} finally {
setLoading(false); // runs on success or failure
}Throwing Errors
throw new Error()Throw a new Error with a descriptive message.
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}Error objects, not stringsYou can throw any value, but Error objects are best practice.
// Avoid: throw 'something went wrong';
// Prefer: throw new Error('something went wrong');Built-in error typesThrow built-in error types for more specific problems.
function setAge(age) {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0 || age > 150) {
throw new RangeError('Age out of valid range');
}
}Custom Error Classes
Extending ErrorCreate custom error types by extending the built-in Error class.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}instanceof checksCheck error type with instanceof in catch blocks.
try {
if (!email.includes('@')) {
throw new ValidationError('Invalid email');
}
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation error:', error.message);
}
}Extra propertiesAdd extra properties to custom errors for richer context.
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = 'HttpError';
this.status = status;
}
}
throw new HttpError(404, 'Resource not found');Async Error Handling
try...catchCatches errors thrown during asynchronous operations when combined with await.
async function loadUser(id) {
try {
const res = await fetch(`/users/${id}`);
return await res.json();
} catch (err) {
console.error(err.message);
return null;
}
}Multiple awaitsGroups multiple sequential asynchronous actions into a single error handler block.
try {
const user = await getUser();
const orders = await getOrders(user.id);
console.log(orders);
} catch (err) {
console.error("Chain failed", err);
}finallyGuarantees resource cleanup or UI state resets regardless of try-catch outcomes.
setLoading(true);
try {
await saveData(form);
} catch (err) {
showError(err.message);
} finally {
setLoading(false);
}Tips
- Create custom error classes to identify error types in catch blocks — makes branching logic far clearer than checking messages.
- Use finally blocks to release resources like file handles or database connections, since they run regardless of errors.
- Re-throw an error after logging it so calling code further up the stack still gets a chance to handle it.
Warnings
- Never swallow errors silently with an empty catch block — always log or handle them so bugs don't disappear.
- Throwing a plain string instead of an Error object loses the automatic stack trace, making bugs harder to track down.
- A return statement inside finally silently overrides any return or thrown error from the try or catch block above it.
In Practice
Combines a custom ValidationError, try/catch/finally, and instanceof checks.
- ValidationError extends Error for instanceof checks.
- Invalid input throws early, separating validation from network errors.
- The catch block branches using instanceof for targeted error handling.
- finally guarantees loading/status reset regardless of outcome.
class ValidationError extends Error {
constructor(msg) {
super(msg);
this.name = 'ValidationError';
}
}
async function getUser(id) {
if (typeof id !== 'number') throw new ValidationError('Invalid ID');
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
err instanceof ValidationError ? console.error('Input:', err.message) : console.error('Fetch:', err.message);
return null;
} finally {
console.log('Done');
}
}FAQ
Yes — wrap your await call inside a try/catch block and it will catch rejected promises just like synchronous errors. Alternatively, chain .catch() on the promise, but try/catch keeps async error handling visually consistent with synchronous code.
Yes, finally always executes before the function actually returns, even if try or catch contains a return. Be careful not to place a return inside finally itself. It overrides any return value from try or catch.
Always throw an Error object (or a subclass), never a plain string. Error objects capture a stack trace automatically, which is essential for debugging. Thrown strings produce no stack trace and are much harder to track down.
Use instanceof to branch on the error's class: if (err instanceof ValidationError) handles it differently from if (err instanceof NetworkError). This is why custom error classes are worth defining. Checking err.message with string matching is fragile and breaks as messages change.