VOOZH about

URL: https://blog.logrocket.com/javascript-try-catch/

⇱ How to use try...catch for error handling in JavaScript - LogRocket Blog


2025-03-27
1403
#javascript
Ivy Walobwa
202615
116
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 How To Use Try...catch For Error Handling In JavaScript

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.

🚀 Sign up for The Replay newsletter

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.

The basic syntax of try...catch

The try...catch statement consists of three key parts:

  • A try block — Contains the code that might throw an error
  • A catch block — Handles an error if one occurs. It’s only executed when an error is thrown
  • A finally block — Runs the cleanup code. It’s executed whether an error is thrown or not

The 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 error
  • errorVar.message – Provides a human-readable error description

The 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

Throwing custom errors

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'
 // }

Conditional catch blocks

Conditional 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

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
 }
 }

Nested try…catch block

A 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

Handling asynchronous errors

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}`);
 }
 

Using finally for cleanup

The 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
 }

Conclusion

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.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

How to build a virtual engineering team with Gemini CLI subagents

Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.

👁 Image
Emmanuel John
Jun 18, 2026 ⋅ 10 min read

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

👁 Image
Emmanuel John
Jun 17, 2026 ⋅ 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

👁 Image
Chizaram Ken
Jun 16, 2026 ⋅ 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read
View all posts

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 now