try...catch
for error handling in JavaScriptBuilding 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.
try...catch
The 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.
Would you be interested in joining LogRocket's developer community?
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 nowDiscover how to use TanStack Table, formerly known as React Table, to build a table UI for a variety of use cases.
Explore what prototypes are, how the prototype chain works, and how to use the prototype chain to create inheritance between objects.
Set up TypeScript with Node.js and Express, focusing on configuring key elements for a smooth development experience.
Examine the differences between the .ts and .tsx file types, their use cases, and best practices for a React TypeScript project.