try...catch for error handling in JavaScript
Building JavaScript applications involves anticipating and handling unexpected issues. Errors are inevitable, but managing them effectively ensures a better user experience. JavaScript provides the try…catch block as a structured way to handle errors gracefully.
This article will explore how to use the try…catch block, covering its basic syntax and advanced scenarios, such as nested blocks, rethrowing errors, and handling asynchronous code.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
try...catchThe try...catch statement consists of three key parts:
try block — Contains the code that might throw an errorcatch block — Handles an error if one occurs. It’s only executed when an error is thrownfinally block — Runs the cleanup code. It’s executed whether an error is thrown or notThe try block must be followed by either a catch or finally block, or both as shown below:
// try...catch
try{
console.log("executing try block...")
console.log(missingVar)
}catch{
console.log("an error occured")
}
// OUTPUT:
// executing try block...
// an error occured
// try...finally
try{
console.log("executing try block...")
}finally{
console.log("final statement")
}
// OUTPUT:
// executing try block...
// final statement
// try...catch...finally
try{
console.log("executing try block...")
console.log(missingVar)
}catch(errorVar){
console.log("an error occured",errorVar)
}finally{
console.log("final statement")
}
// OUTPUT:
// executing try block...
// an error occured
// final statement
The catch block has an error identifier that can be used to access the thrown error. You can access it as a whole (e.g, errorVar) or use its properties individually:
errorVar.name – Specifies the type of errorerrorVar.message – Provides a human-readable error descriptionThe code snippet below uses destructuring to access the error thrown:
try {
console.log(missingVar)
} catch ({name, message}) {
console.log("name: ", name)
console.log("message: ", message)
}
// OUTPUT:
// name: ReferenceError
// message: missingVar is not defined
Sometimes, built-in errors like TypeError don’t fully capture what went wrong. Throwing custom errors allows you to provide clearer error messages, and include additional debugging information.
To create a custom error, you extend the Error class, define a constructor that sets a meaningful error message, and assign a custom name. You can optionally include additional debugging information and capture the original stack trace for debugging on development:
class OperationError extends Error {
/**
* Custom error for handling operation failures.
* @param {string} resource - The resource involved in the error.
* @param {string} action - The action that failed.
*/
constructor(resource, action) {
// Construct a meaningful error message
super(`Failed to ${action} ${resource}. Please check the resource and try again.`);
// Preserve the original stack trace (optional, useful for debugging)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, OperationError);
}
this.name = "OperationError";
// Custom debugging information
this.resource = resource;
this.action = action;
}
}
In the code snippet below, the custom error is thrown in the try block to simulate a function call that may encounter this specific type of error. The error object includes the stack trace and additional error properties:
try {
// simulate an operation that may throw an exception
throw new OperationError("file", "read");
} catch (error) {
console.error(`${error.name}: ${error.message}`);
console.log(`additional info:resource was a ${error.resource} and action was ${error.action}`)
console.log(error)
}
// OUTPUT:
// OperationError: Failed to read file.Please check the resource and try again.
// additional info:resource was a file and action was read
// OperationError: Failed to read file.Please check the resource and try again.
// at Object.< anonymous > (/Users/walobwa / Desktop /project / test.js: 25: 11)
// at Module._compile(node: internal / modules / cjs / loader: 1376: 14)
// at Module._extensions..js(node: internal / modules / cjs / loader: 1435: 10)
// at Module.load(node: internal / modules / cjs / loader: 1207: 32)
// at Module._load(node: internal / modules / cjs / loader: 1023: 12)
// at Function.executeUserEntryPoint[as runMain](node: internal/modules/run_main: 135: 12)
// at node: internal / main / run_main_module: 28: 49 {
// resource: 'file',
// action: 'read'
// }
catch blocksConditional catch blocks use the if...else statement to handle specific errors while allowing unexpected ones to propagate.
Knowing the different types of errors that can be thrown when executing code helps handle them appropriately. Using instanceof, we can catch specific errors like OperationError and provide a meaningful message for the error:
try {
// simulate an operation that may throw an exception
throw new OperationError("file", "read");
} catch (error) {
if (error instanceof OperationError) {
// handle expected error
console.error("Operation Error encountered:", error.message);
} else {
// log unexpected error
console.error("Unexpected error encountered:", error.message);
}
}
// OUTPUT:
// Operation Error encountered: Failed to read file. Please check the resource
// and try again.
In the code snippet above, we log any other error in the else statement. A good practice would be to rethrow errors not explicitly handled in the try...catch block.
Rethrowing errors ensures that they are propagated up the call stack for handling. This prevents silent failures and maintains the stack trace.
In the code snippet below, we catch the expected error, OperationError, silence it, and then defer the handling of other errors by rethrowing. The top-level function will now handle the rethrown error:
try {
throw new TypeError("X is not a function");
} catch (error) {
if (error instanceof OperationError) {
console.error("Operation Error encountered:", error.message);
} else {
throw error; // re-throw the error unchanged
}
}
try…catch blockA nested try...catch block is used when an operation inside a try block requires separate error handling. It helps manage multiple independent failures, ensuring one failure does not disrupt the entire execution flow.
Errors in the inner block are caught and handled locally while the outer block manages unhandled or propagated errors. If the error thrown is handled in the inner try..catch block, the outer catch block is not executed:
try {
try {
throw new OperationError("file", "read");
} catch (e) {
if (e instanceof OperationError) {
console.error("Operation Error encountered:", e.message);
} else {
throw e; // re-throw the error unchanged
}
} finally {
console.log("finally inner block");
}
} catch (err) {
console.error("outer error log", err.message);
}
// OUTPUT:
// Operation Error encountered: Failed to read file. Please check the resource and // try again.
// finally inner block
If an error is not handled or is rethrown in the inner block, the outer try...catch block catches it. The nested finally block executes before the outer catch or finally block, ensuring cleanup at each level:
try {
try {
throw new TypeError("file");
} catch (e) {
if(e instanceof OperationError) {
console.error("Operation Error encountered:", e.message);
} else {
throw e; // re-throw the error unchanged
}
} finally {
console.log("finally inner block");
}
} catch (err) {
console.error("outer error log", err.message);
}
// OUTPUT:
// finally inner block
// outer error log file
try...catch works with synchronous code. When an error occurs inside an asynchronous function, the try...catch block completes execution before the error occurs, leaving it unhandled.
Asynchronous operations require proper error handling to prevent unhandled rejections and unexpected failures. Using try...catch with async/await helps prevent unhandled rejections from slipping through.
async/await ensures that the try…catch block waits for the result of the asynchronous operation before proceeding:
async function openFile(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new OperationError("file", "open"); // Reusing OperationError for handling file open errors
}
return await response.json();
} catch (error) {
console.error(`File fetch failed: ${error.message}`);
// Rethrow or handle gracefully
throw error; // Propagate the error upward
}
}
In the example above, the openFile function is asynchronous. The result of the fetch operation is awaited. If an error is thrown, it is logged and propagated to the outer try...catch block where it’s handled:
try {
const data = await openFile("data.json");
console.log(data);
} catch (error) {
console.error(`Failed to open file: ${error.message}`);
}
finally for cleanupThe finally block in a try...catch statement is used to execute code that must run regardless of whether an error occurs. This is useful for cleanup operations such as closing files, releasing resources, or resetting states:
try {
// operation that oppens file and throws operaion error
throw new OperationError("file", "read");
} catch (error) {
if(error instanceof OperationError) {
console.error(`Operation error: ${error.message}`);
} else {
throw error;
}
} finally {
closeFile(file); // Ensures the file is closed even if an error occurs
}
This tutorial explored error handling in JavaScript using the try...catch block. We covered its basic syntax, throwing custom errors, rethrowing errors, and using nested blocks. We also discussed handling asynchronous errors with try...catch and async/await, as well as using the finally block for code cleanup.
By effectively using try...catch, developers can build more robust applications, prevent unexpected crashes, and improve debugging, ensuring a better user experience.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.

Kombai AI converts Figma designs into clean, responsive frontend code. It helps developers build production-ready UIs faster while keeping design accuracy and code quality intact.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 22nd issue.

John Reilly discusses how software development has been changed by the innovations of AI: both the positives and the negatives.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now