JavaScript Error Handling
Handle errors gracefully in JavaScript using try/catch, custom error classes, and finally blocks.
TL;DR
- 01Wrap risky code in
try/catchto handle thrown errors cleanly. - 02Throw custom error classes to make
catchblocks more precise. - 03Use
finallyto run cleanup code regardless of success or failure.
Tips
- 01Create custom error classes to identify error types in catch blocks — makes branching logic far clearer than checking messages.
- 02Use finally blocks to release resources like file handles or database connections, since they run regardless of errors.
- 03Re-throw an error after logging it so calling code further up the stack still gets a chance to handle it.
Warnings
- 01Never swallow errors silently with an empty catch block — always log or handle them so bugs don't disappear.
- 02Throwing a plain string instead of an Error object loses the automatic stack trace, making bugs harder to track down.
- 03A return statement inside finally silently overrides any return or thrown error from the try or catch block above it.
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
}Execution stops at throwCode inside try after the thrown line does not execute.
try {
throw new Error('stop here');
console.log('never runs');
} catch (e) {
console.log(e.message); // "stop here"
}Optional catch bindingOmit the catch binding if you don't need the error object.
try {
mayFail();
} catch {
// optional binding — no variable needed
console.log('Something went wrong');
}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 without an errorFinally runs even when there is no error in try.
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
}
}Releasing resourcesUse finally to release resources like connections or file handles.
let connection;
try {
connection = openDB();
return connection.query('SELECT * FROM users');
} finally {
connection?.close();
}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');
}
}Re-throwingRe-throw errors after logging to let upstream code handle them.
try {
riskyOp();
} catch (e) {
logger.error(e);
throw e; // propagate to caller
}Throwing inside catchThrowing inside a catch block escalates the error upstream.
catch (error) {
if (error instanceof SyntaxError) {
throw new Error('Config file is malformed');
}
}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');Multiple error classesUse multiple custom error classes to categorize problems.
class NetworkError extends Error { }
class AuthError extends Error { }
class NotFoundError extends Error { }Branching in catchHandle specific error types separately, letting unknown errors bubble up.
catch (error) {
if (error instanceof AuthError) return redirectToLogin();
if (error instanceof NetworkError) return showRetry();
throw error; // unknown errors bubble up
}Common Error Types
SyntaxErrorOccurs when code or data cannot be parsed.
try {
JSON.parse('invalid json');
} catch (error) {
if (error instanceof SyntaxError) {
console.log('Invalid JSON format');
}
}TypeErrorOccurs when a value is used with the wrong type.
try {
const x = null;
x.method(); // TypeError: Cannot read properties of null
} catch (e) {
console.log(e instanceof TypeError); // true
}ReferenceErrorOccurs when a variable is not defined.
try {
console.log(undeclaredVar);
} catch (e) {
console.log(e instanceof ReferenceError); // true
}RangeErrorOccurs when a number falls outside valid bounds.
try {
new Array(-1); // RangeError: Invalid array length
} catch (e) {
console.log(e instanceof RangeError); // true
}error.nameCheck error names as a string alternative to instanceof.
catch (error) {
console.log(error.name); // "TypeError", "RangeError", etc.
if (error.name === 'TypeError') handleTypeError(error);
}In Practice
Combines a custom ValidationError class, try/catch/finally, and instanceof checks to handle distinct failure modes cleanly.
- 01ValidationError extends Error so it carries a stack trace and can be caught with instanceof.
- 02Invalid input throws before the fetch even starts, keeping validation separate from network errors.
- 03The catch block branches on instanceof to give validation and network failures different handling.
- 04finally always resets the loading state, whether the request succeeded, failed, or never started.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
async function getUser(id) {
if (typeof id !== 'number') {
throw new ValidationError('id must be a number');
}
setLoading(true);
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return await res.json();
} catch (error) {
if (error instanceof ValidationError) {
console.error('Bad input:', error.message);
} else {
console.error('Fetch failed:', error.message);
}
return null;
} finally {
setLoading(false);
}
}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.
A ReferenceError means you accessed a variable that doesn't exist in scope. A TypeError means a value exists but you're using it in an incompatible way, like calling null as a function. Recognizing which one you have narrows down the likely cause of the bug.