try-catch-finally blocks in Javascript

Anton Ioffe - September 13th 2023 - 15 minutes read

JavaScript, as the foundation of contemporary web development, has an array of techniques and structures at its disposal through which developers can handle exceptions, errors and unpredictable behaviours of code execution. One such powerful instrument it offers is the ‘try-catch-finally’ structure which we are going to delve deeper into in this article. Regardless of what you already know, this composition will shore up your understanding, challenge your existing perspectives, and introduce you to a few advanced techniques that will boost your problem-solving skills in the wild.

The interesting part is that we won't stop at the simple syntax and typical usage of these structures. Expect a broad exploration that encompasses their relation to other JavaScript constructs, such as Promises and async-await; and the unusual but equally potent usage scenarios, such as embedding multiple catch or finally blocks after a single try. By tackling the common myths about try-catch-finally, we aim to offer you a clear, well-rounded understanding that underpins effective coding practice.

So whether you're seeking a refresher on these structures, or you're ready to dig deeper and uplevel your error-handling capabilities, this is the article for you. Dive in, take notes, and let's illuminate the intricacies of the 'try-catch-finally' blocks in JavaScript.

Understanding the basics of Javascript's try-catch-finally structure

Understanding the try-catch-finally construct in JavaScript entails handling errors or exceptions, which are inevitable in programming. These blocks bring control to situations that can occur during the execution of the program.

Try Block Syntax and Usage

The try block contains the code segment that may potentially throw an error. The syntax is as follows:

try {
    // Code that may throw an error
    riskyOperation();
} 

In this code, riskyOperation() is a hypothetical function that can throw an error. Placing this function in the try block allows errors to be caught and handled gracefully.

Catch Block Syntax and Usage

The catch block is designed to manage exceptions thrown in the try block. The syntax is as follows:

try {
    // Code that may throw an error
    riskyOperation();
} catch(error) {
    // Code to handle the error
    handleException(error);
}

Here error is the exception thrown by the code within the try block. The catch block will execute handleException(error), where the error handling operation takes place.

Finally Block Syntax and Usage

The finally block executes after the try and catch blocks regardless of the result. The syntax is as follows:

try {
    // Code that may throw an error
    riskyOperation();
} catch(error) {
    // Code that handles the error
    handleException(error);
} finally {
    // Code that runs regardless of the outcome
    alwaysRunThis();
}

In the above example, alwaysRunThis() will execute regardless of whether riskyOperation() throws an error.

Common Mistakes

A commonly made mistake is to assume that if an error is thrown, the rest of the code within the try block after that line will continue to execute. But that's not how it works. When a line throws an error, the execution of the try block halts immediately and control transfers to the catch block.

Incorrect Code:

let a = 5;
try {
    a *= n; // Throws an error, `n` is not defined
    a++; // This line will not execute
} catch(e) {
    console.log(e);
}

Correct Code:

let a = 5;
try {
    a = increment(a); // assumes increment function exists
} catch(e) {
    console.log(e);
}

Remember

Understanding the try-catch-finally structure in JavaScript is vital to handle exceptions and create robust code. Ensure to handle exceptions in the catch block and put cleanup code or code that must always run in the finally block. Remember that as soon as a line of code in the try block throws an error, the rest of the try block will not execute.

Using multiple catch and finally blocks with one try

Deploying Multiple Catch and Finally Blocks with a Single Try Block

The robust error handling mechanisms in Javascript provide developers with a disciplined way of avoiding critical application crashes. These constructs - try, catch, and finally blocks - are typically placed around code that might fail and are treated as potential tripwires that spring into action when an error is encountered. In this section, we will guide you through the practice of using multiple catch and finally blocks with a single try block.

Prevailingly, a typical try-catch-finally structure is composed of a single try block sandwiched between matching catch and finally blocks. However, the actual design of Javascript allows multiple catch blocks and multiple finally blocks to be stacked up against a single try block. Let's dive deeper into this approach.

The Multiple Catch Approach

Let's initially examine how we could employ multiple catch blocks. Consider the subsequent example:

try {
    riskyOperation();
} catch (error) {
    if (error instanceof TypeError) {
        handleTypeError(error);
    } else if (error instanceof RangeError) {
        handleRangeError(error);
    } else {
        handleGenericError(error);
    }
}

In this instance, we note that the segment within the catch block is structured to handle different error types. Different handlers are called based on the error instance type. Although not technically "multiple catch blocks", this effectively accomplishes the same goal.

Common Mistake: One common mistake is assuming that multiple catch blocks can be directly stacked up against a single try block. But this is erroneous as represented here:

try {
    riskyOperation();
} catch(e if e instanceof TypeError) {
    //handle TypeError
} catch(e if e instanceof RangeError) {
    // handle RangeError
} 

This will generate a SyntaxError. Contrary to languages like Java, Javascript doesn't natively support multiple catch blocks.

Employing Multiple Finally Blocks

Next, we shall discuss employing multiple finally blocks. Even though Javascript specification does not include multiple finally blocks directly following a single try block, we can encapsulate additional try-catch-finally structures inside the outer one. Here's an example:

try {
    riskyOperation();
} catch (error) {
    handleError(error);
} finally {
    try {
        cleanupOperation1();
    } finally {
        cleanupOperation2();
    }
}

In this scenario, cleanupOperation1() is always executed, irrespective of whether riskyOperation() succeeds or fails. And in the nested finally block, cleanupOperation2() is always executed after cleanupOperation1(), regardless of whether cleanupOperation1() throws an error.

Common Mistake: Sometimes, two separate cleanups might be written as follows:

try {
    riskyOperation();
} catch (error) {
    handleError(error);
} finally {
    cleanupOperation1();
} finally {
    cleanupOperation2();
}

Again, this won't work. This will cause a SyntaxError as Javascript does not natively support multiple finally blocks.

If you are wondering why "multiple" finally blocks are necessary when a single finally block can execute multiple statements, rest assured that circumstances may arise where you could benefit from this advanced structure. For example, consider if cleanupOperation1() may throw an error that must not stop cleanupOperation2() from executing. In such a case, this would be the ideal approach.

In conclusion, Javascript error handling necessitates a keen expertise in the nuances of its syntax. With this exploration into multiple catch and finally blocks, you can elegantly and efficiently handle a wider array of error scenarios. While not directly supported, the ability to emulate multiple catch and finally blocks allows you to handle a range of different types of errors in the catch clause, such as TypeError and RangeError. Similarly, you can ensure all critical cleanups operations are performed, despite any issues occurring during cleanup. Finally, always remember that improper understanding or use of these constructs can lead to common mistakes like stacking multiple catch or finally blocks, which can result in a Syntax error.

Error types and exceptions in Javascript

In JavaScript, errors that occur during the execution of the program are known as exceptions. When such events disrupt the natural flow of your program, it is inevitable for an error message to be thrown. But with the power of try-catch blocks, developers can decide how the program will respond to these exceptions.

Error Types in Javascript

JavaScript has several in-built error constructors, each intended to handle different types of errors:

  1. Error: This is the parent of all other error types. It can be used to create a generic error.
try {
   throw new Error('A general error has occurred');
} catch (e) {
   console.log(e.message);
}
  1. RangeError: This is thrown when you try to pass a value as an argument that is not in the acceptable range.
try {
  new Array(-1);
} catch (e) {
  console.log(e instanceof RangeError); // true
  console.log(e.message);                // "Invalid array length"
}
  1. ReferenceError: This occurs when you try to use a variable that has not been declared.
try {
  x++;
} catch (e) {
  console.log(e instanceof ReferenceError); // true
}
  1. SyntaxError: The SyntaxError object indicates an error when trying to interpret syntactically incorrect code.
try {
  eval('hoo bar');
} catch (e) {
  console.log(e instanceof SyntaxError); // true
}
  1. TypeError: This is thrown when a variable or parameter is not of a valid type.
try {
  null.f();
} catch (e) {
  console.log(e instanceof TypeError); // true
}
  1. URIError: This is thrown when the URI functions are used in an incorrect way.
try {
  decodeURIComponent('%');
} catch (e) {
  console.log(e instanceof URIError); // true
}

Try-Catch vs If-Else Comparison

Try-catch and if-else can often be used interchangeably, but one notable difference is how they handle unexpected, runtime errors.

If-else structures are used to control program flow based on predefined conditions. They are useful when you want to execute different code blocks for different conditions, but they don’t handle errors that arise during execution of their code blocks.

On the other hand, try-catch blocks can catch unexpected errors at runtime and lets you handle them gracefully.

Consider these two pieces of code where one uses if-else and the other uses try-catch:

// Using if-else
let car = {brand: 'Toyota', model: 'Camry'};
if (car.brand !== undefined) {
   console.log(`The car brand is: ${car.brand}`);
} else {
  console.log('Information is missing!');
}

// Using try-catch
let car = {brand: 'Toyota', model: 'Camry'};
try {
  console.log(`The car brand is: ${car.brand}`);
} catch (e) {
  console.log(`An error occurred: ${e.message}`);
}

In the first example, if the car.brand is undefined, it simply prints 'Information is missing!'. But if an error arises outside of the car.brand check (let’s say console.log fails due to issues in the JavaScript engine), the error cannot be handled within this code.

In contrast, the try-catch block in the second example can handle failure in console.log or any other error within its scope. When an error happens, the code execution moves instantly to the catch block, giving a chance to handle the error gracefully and debug it easily.

While the try-catch block might seem more appealing due to its broad error-catching mechanism, you should use it judiciously. Unnecessary try-catch blocks can become a performance hindrance and also make the code harder to debug and maintain, due to nature of JavaScript's error handling.

Common Code Mistakes and their Correction

A common mistake when working with try-catch blocks is for the catch block to suppress the error entirely without any form of handling. This is a terrible practice as it could hide relevant debugging information when an error occurs.

// Incorrect
try {
  riskyOperation();
} catch (e) {
  // Oops! swallowed all errors
}

// Correct
try {
  riskyOperation();
} catch (e) {
  console.error(e); // always log or handle the error in some way
}

Remember that it's always crucial to at least log the error details for later debugging.

Another common mistake is not understanding the concept of error bubbling. If an error is thrown inside a function without a try-catch block, the error bubbles up to the containing code. If that code also lacks a try-catch, the error continues to bubble up all the way to the top level of your code. If no appropriate catch block is found, it results in an 'uncaught exception'.

// Incorrect
function a() {
  b();
}

function b() {
  c();
}

function c() {
  throw new Error('Error in c()');
}

try {
  a();
} catch (e) {
  console.error(e);
}

// Correct
function a() {
  try {
    b();
  } catch (e) {
    throw e; // or handle the error here
  }
}

function b() {
  try {
    c();
  } catch (e) {
    throw e; // or handle the error here
  }
}

function c() {
  throw new Error('Error in c()');
}

try {
  a();
} catch (e) {
  console.error(e);
}

Question for you to ponder - how would a JavaScript program featuring both if-else and try-catch blocks handle an inadvertent error in one of the if-else statements? Would it make a difference if an else statement was absent?

Would you write more error specific catch blocks or a single one to handle all error types in your JavaScript code? What would be the trade-offs to consider in both scenarios?

Advanced usage of try-catch-finally block

One of the main advantages of using try-catch-finally blocks in JavaScript is that they provide a structured method to handle different error types. However, even seasoned developers sometimes struggle with the advanced usage of these blocks. In this section, we will dive deep into complex layers of the try-catch-finally blocks such as nested blocks, deploying blocks without a catch or finally, and whether such usage is indicated as bad practice.

Nested try-catch-finally Blocks

Nested blocks allow developers to pinpoint specific locations for each error trap, making the error messages more specific. They can create a hierarchy of error handling responsibilities where inner blocks take care of localized incidents, reducing noise for external blocks. See the example code block:

try {
    // outer try block
    try {
        // inner try block
        throw new Error('1');
    } catch(e) {
        // inner catch
        console.log('caught ' + e.message); // Output: caught 1
        throw new Error('2'); // re-throw for outer catch
    }
} catch(e) {
    // outer catch
    console.log('caught ' + e.message); // Output: caught 2
} finally {
    console.log('Wrapped up both try-catch blocks'); // This will always run
}

Here, the inner catch(e) block catches an error from the inner try statement and then re-throws a new error for the outer catch(e) block.

Blocks without catch or finally

It is possible to use a try block without a catch or finally block as shown in the following example:

try {
    console.log('I will always run');
    throw 'I am an error';
}

Common Mistake: Not understanding that a try block without a catch or finally will not handle errors. The thrown error will not be caught here, causing a runtime error, and crashing your script.

Correct Practice: Always accompany a try block with at least a catch or finally block to ensure the error is handled in some manner.

Is It Bad Practice?

The use of try-catch-finally blocks without a catch or finally can lead some to wonder: Is this a bad practice?

It's not necessarily bad practice - it's more of a caution practice. If you have a try block alone without a catch or finally, it means you are not interested in error handling, or that the error handling may have been managed elsewhere in the code. Nevertheless, it's not advisable as standard code errors will go unhandled, and it could lead to script crashes.

Consider following up the catch block with a finally block, as finally executes regardless of an error occurring or not. It often serves as a clean up or guarantee that some piece of code will always run.

In Conclusion

Always consider using catch or finally with try blocks to avoid unhandled code errors and crashes. Using nested try-catch-finally blocks could assist you in catching and handling specific errors in a better way and provide a better debugging output.

Evaluate these details while writing your code and review how nested blocks and blocks without a catch or finally can create a different effect on your code execution. Ask yourself: how comprehensive does your error-handling need to be? Does your current try-catch-finally utilization meet your application's demands?

Knowledge of advanced try-catch-finally usage doesn't just mark you as a competent JavaScript programmer, but also amplifies your capacity to write reliable and maintainable code.

Promises with try-catch and relation to async/await

Let's delve deeper into the pertinent role of try-catch when working with promises and its interplay with async/await.

Promises and try-catch

It's widely acknowledged that promises provide respite from callback hell in JavaScript, reliably encapsulating the outcome of an asynchronous operation. Yet, error handling from promises can prove to be a convoluted affair, particularly when tied in with try-catch blocks.

Promise catch and finally methods exhibit significant differences. Let's illuminate this with a basic promise scenario:

let myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Promise Resolved');
    }, 1000);
});

myPromise
    .then(result => console.log(result))
    .catch(error => console.log(error))
    .finally(() => console.log('Cleanup Code'));

In this traditional promise configuration, catch() is set in place to deal with any errors that may occur during the promise resolution or rejection process. Conversely, finally() is charged with conducting after-settlement cleanup — independent of whether the promise was fulfilled or rejected.

Comparing try-catch and then-catch

Naturally, one might query: why employ try-catch when then-catch is available? The response hinges on your code organization approach and the granularity of error handling you are targeting.

Consider this piece of code:

myPromise
    .then(result => {
        throw new Error('Error in the then block');
        console.log(result);
    })
    .catch(error => console.log(error.message));

An error sprung up in the then block is swiftly captured by the subsequent catch block. However, substituting the then-catch sequence with a try-catch construct results in:

try {
    myPromise
        .then(result => {
            throw new Error('Error in the then block');
            console.log(result);
        });
} catch(error) {
    console.log(error.message);
}

Surprisingly, the error slips through. This occurs because JavaScript's try-catch mechanism is solely wired for synchronous code. Given that our promise resolves post the try-catch block execution, the error remains uncaptured.

Syncing with async-await and Enhancing Code Readability

Enter the async-await syntax. It facilitates delivering better readability and structure while using try-catch with promises. To illustrate this:

async function myFunction() {
    try {
        let result = await myPromise;
        console.log(result);
    } catch(error) {
        console.log(error.message);
    }
}

myFunction();

The await keyword here halts the flow until the promise is either resolved or rejected. If an exception gets thrown in the meantime, it's neatly intercepted by the catch block. It contributes to enhancing code readability by lending a synchronous facade to asynchronous code, without forfeiting JavaScript's non-blocking nature.

However, it's vital to note that pairing async-await with try-catch isn't entirely devoid of pitfalls. Here's a common misstep:

// Incorrect
async function myFunction() {
    try {
        let result = myPromise;  // Missed using 'await'
        console.log(result);
    } catch(error) {
        console.log(error.message);
    }
}

// Correct
async function myFunction() {
    try {
        let result = await myPromise; 
        console.log(result);
    } catch(error) {
        console.log(error.message);
    }
}

Excluding the await keyword while drafting your promise will disrupt error handling. Ensure the await keyword's application while working with async-await to fully exploit its error handling facilities.

In summary, mastering the interconnection between try-catch and promises in JavaScript, amplified with the aid of async-await, can be incredibly valuable for managing asynchronous procedures and error handling. By identifying these relationships, you can architect clean, readable code that is easy to modify and maintain. It's crucial to recognize that the choice of technique largely depends on your code's specific requirements and the context of your work. Therefore, understanding how these constituents impact the following would be advantageous:

  • Performance
  • Memory
  • Complexity
  • Readability
  • Modularity
  • Reusability

Emphasis should be placed on the fact that the catch() method for promises and the catch block in try-catch serve divergent purposes. While the former traps promise rejections, the latter is designed to catch synchronous errors.

Summary

The article provides a comprehensive analysis of try-catch-finally blocks in JavaScript, delving into their basic and advanced usage, common errors and how they interact with other JavaScript constructs like Promises and async-await. It introduces the concept of nested try-catch-finally blocks and their advantages, and warns against the usage of try block alone without a catch or finally, which could lead to script crashes. The article also details the difference between Error types in Javascript and the distinction between using if-else structures and try-catch blocks for handling unexpected runtime errors.

Notably, the article draws a clear delineation between promise catch and finally methods as compared to try-catch blocks. It illustrates the advantage of async-await in enhancing code readability and synchronized handling of promise-based operations within try-catch blocks. Ultimately, the choice between try-catch blocks and promise-based error handling methods should be determined by specific requirements of one's code, performance considerations, and how each contributes to code readability, modularity and reusability.

As a challenging task for further exploration, consider creating a complex program that handles various types of exceptions and employ logging for each error type. Additionally, consider using nested try-catch-finally blocks, handling multiple errors by throwing them from inner try-catch to an outer block and how you can mix it with async-await syntax for dealing with promises. Keep in mind the trade-offs between readability and performance while doing so.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers