JavaScript Try Catch: A Complete Guide

updated on 12 September 2022

In any software development project, handling errors and exceptions is vital to the success of your application. Whether you’re a novice or an expert, your code can fail for various reasons, like a simple typo, upstream errors, or unexpected behavior from external services.

Because there is always the possibility of your code failing, you need to prepare to handle that situation by making your code more robust. You can do this in multiple ways, but one common solution is by leveraging try-catch statements. This statement allows you to wrap a block of code to try and execute. If any errors occur during this execution, the statement will *catch* them, and you can fix them quickly, avoiding an application crash.

This guide will serve as a practical introduction to try-catch statements and will show you how you can use them to handle errors in Javascript.

Why You Need Try-Catch

Before learning about try-catch statements, you need to understand error handling as a whole in JavaScript.

When dealing with errors in JavaScript, there are several types that can occur. In this article, you’ll focus on two types: syntax errors and runtime errors. Learn more about syntax errors in our other blog post here.

Syntax errors occur when you don’t follow the rules of a specific programming language. They can be detected by configuring a linter, which is a tool used to analyze the code and flag stylistic errors or programming errors. For instance, you can use ESLint to find and fix problems in your code.

Following is an example of a syntax error:

console.log(;)

In this case, the error occurs because the syntax of the code is incorrect. It should be as follows:

console.log('Some Message');

A runtime error happens when a problem occurs while your application is running. For example, your code may be trying to call a method that doesn’t exist. To catch these errors and apply some handling, you can use the try-catch statement.

What Is an Exception

Exceptions are objects that signal a problem has occurred at the execution time of a program. These problems can occur when accessing an invalid array index, when trying to access some member of a null reference, when trying to invoke a function that does not exist, etc.

For example, consider a scenario where your application relies on an upstream third-party API. Your code might handle responses from this API, expecting them to contain certain properties. If this API returns an unexpected response for whatever reason, it can possibly trigger a runtime error. In this case, you can wrap the affected logic in a try-catch statement and provide the user with an error message or even invoke some fallback logic rather than allow the error to crash your application.

 How Does Try-Catch Work

Simply put, a try-catch statement consists of two blocks of code—one prefixed with the try keyword and the other with the catch keyword—as well as a variable name to store the error object within. If the code inside the try block throws an error, the error will be passed to the catch block for handling. If it doesn’t throw an error, the catch block will never be invoked.

Consider the following example:

try {
  nonExistentFunction()
} catch (error) {
  console.log(error); // [ReferenceError: nonExistentFunction is not defined]
}

In this example, when the function is called, the runtime will find that there is no such function and will throw an error. Thanks to the try-catch statement surrounding it, the error is not fatal and can instead be handled however you like. In this case, it is passed to console.log, which tells you what the error was.

There is also another optional statement called the finally statement, which, when present, is always executed after both try and catch, regardless of whether catch was executed or not. It’s often used to include commands that release resources that may have been allocated during the try statement and may not have had a chance to gracefully clean up in the event of an error. Consider the following example:

openFile();
try {
   writeData();
} catch (error) {
   console.log(error);
} finally {
   closeFile();
}

In this contrived example, imagine a file handle is opened prior to the try-catch-finally statement. If something were to go wrong during the try block, it’s important that the file handle still be closed so as to avoid memory leaks or deadlocks. In this case, the finally statement ensures that regardless of how the try-catch plays out, the file will still be closed before moving on.

Of course, wrapping your potentially error-prone code in try-catch statements is only one piece of the fault-tolerance puzzle. The other piece is knowing what to do with the errors when they are thrown.

Sometimes it might make sense to display them to the user (typically in a more human-readable format), and sometimes you might want to simply log them for future reference. Either way, it helps to be familiar with the Error object itself so that you know what data you have to work with.

Error Object

Whenever an exception is thrown inside the try statement, JavaScript creates an Error object and sends it as an argument to the catch statement. Typically, this object has two main instance properties:

- name: a string describing the type of error that occurred

- message: a more detailed description of what went wrong

Some browsers also include other properties, like description or fileName, but these are not standard and should typically be avoided because they cannot be reliably used in all browsers. To see what sort of values these properties contain, consider the following example:

try {
   nonExistentFunction();
} catch (error) {
    const {name, message} = error;
    console.log({ name, message }) // { name: 'ReferenceError', message: 'nonExistentFunction is not defined' }
}

As mentioned earlier, the name property’s value refers to the type of error that occurred. The following is a non-exhaustive list of some of the more common types of errors:

- ReferenceError is thrown when a reference to a non-existent or invalid variable or function is detected.

- TypeError is thrown when a value is used in a way that is not compatible with its type, such as trying to call a string function on a number ((1).split(',');).

- SyntaxError is thrown when there is a syntax error in interpreting the code; for example, when parsing a JSON using trailing commas (JSON.parse('[1, 2, 3, 4,]');).

- URIError is thrown when an error occurs in URI handling; for example, sending invalid parameters in decodeURI() or encodeURI().

- RangeError is thrown when a value is not in the set or range of allowed values; for example, a string value in a number array.

All native JavaScript errors are extensions of the generic Error object. Based on this principle, you can also create your own error types.

Custom Errors

Another keyword in JavaScript that is closely related to errors and error handling is throw. When you use this keyword, you're able to  “throw” a user-defined exception. When you do this, the current function will cease execution, and whatever value you used with the throw keyword will be passed to the first catch statement in the call stack. If there are no catch statements to handle it, the behavior will be similar to a typical unhandled error, and the program will terminate.

Throwing custom errors can be useful in more complex applications, as it affords you another avenue of flow control over the code. Consider a scenario where you need to validate some user input. If the input is deemed invalid according to your business rules, you do not want to continue processing the request. This is the perfect use case for a throw statement. Consider the following example:

// you can define your own error types by extending the generic class
class ValidationError extends Error {
  constructor(message) {
       super(message);
      this.name = 'ValidationError';
    }
}
// for demonstrative purposes
const userInputIsValid = false;
try {
    if (!userInputIsValid) {
        // manually trigger your custom error
        throw new ValidationError('User input is not valid');
   }
} catch (error) {
    const { name, message } = error;
    console.log({ name, message }); // { name: 'ValidationError', message: 'User input is not valid' }
}

Here, a custom error class is defined, extending the generic error class. This technique can be used to throw errors that specifically relate to your business logic, rather than just the default ones used by the JavaScript engine.

Should You Wrap Everything in a Try-Catch Statement

Because errors will go up the call stack until they either find a try-catch statement or the application terminates, it might be tempting to simply wrap your entire application in one massive try-catch, or lots of smaller ones, so that you can enjoy the fact that your application will technically never crash again. This is generally not a good idea.

Errors are a fact of life when it comes to programming, and they play an important role in your application’s lifecycle that shouldn’t be ignored. They tell you when something has gone wrong. As such, it’s important to use try-catch statements respectfully and mindfully if you want to deliver a good experience to your users.

You should generally use try-catch statements anywhere you reasonably expect that an error is likely to occur. Once you’ve caught the error, however, you typically don’t want to just squash it. As mentioned previously, when an error is thrown, it means that something has gone wrong. You should take this opportunity to handle the error appropriately, whether that is displaying a nicer error message to the user in the UI or sending the error to your application monitoring tool (if you have one) for aggregation and later analysis.

Typically, when wrapping code in a try-catch statement, you should only wrap code that is conceptually related to the error that you are expecting. If you have functions that are fairly small in size, this might mean that the entire body of the function is wrapped. Alternatively, you may have a larger function where all of the code in the body is wrapped, but spread across multiple try-catch statements. This can actually serve as an indication that the function in question is overly complex and/or handling too many responsibilities. Such code can be a candidate for decomposition into smaller, more focused functions.

For areas in your code where you don’t reasonably expect errors, it’s usually better to forego excessive try-catch statements and simply allow errors to occur. This might sound counterintuitive, but by allowing your code to fail fast, you're actually putting yourself in a better position. Squashing errors might give the appearance of stability, but there will still be underlying issues.

Allowing errors to occur when not explicitly handled means that when paired with an application monitoring tool, like BugSnag or Sentry, you can globally intercept and record errors for later analysis. These tools let you see where the errors in your application actually are so that you can fix them, rather than just blindly ignoring them.

Asynchronous Functions

To understand what asynchronous functions in JavaScript are, you have to understand what Promises are.

Promises are essentially objects that represent the eventual completion of asynchronous operations. They define an action that will be performed in the future and will ultimately be resolved (successfully) or rejected (with an error).

For example, the following code shows a simple Promise that promptly resolves a value. The value is then passed to the then callback:

// This promise will successfully resolve and has no `catch` equivalent
new Promise((resolve, reject) => {
   const a = 10;
   const b = 9;
   resolve(a + b);
})
.then(result => {
    console.log(result); // 19
});

The next example shows how errors that are thrown inside a Promise (either by you or by the JavaScript engine) will be handled by the Promises catch callback:

new Promise((resolve, reject) => {
    const a = 10;
   const b = 9;
   throw new Error('manually thrown error')
    resolve(a + b); // this line is never reached
})
.then(result => {
   console.log(result); // this never happens
})
.catch(error => {
    console.log('something went wrong', error) // Something went wrong [Error: manually thrown error]
})

Rather than manually throwing errors in your Promises, you can instead use the reject function. This function is provided as the second argument to the Promise’s callback:

new Promise((resolve, reject) => {
    const a = 10;
   const b = 9;
   reject('- manual rejection');
   console.log(a + b); // 19
    resolve(a + b); // never called, as you already called `reject`, and a promise cannot resolve AND reject
})
.then(result => {
   console.log(result); // this never happens
})
.catch(error => {
    console.log('something went wrong', error) // something went wrong - manual rejection
})

You may have noticed something odd in the comments on that last example. There is a difference between throwing an error and rejecting a Promise. Throwing an error will stop the execution of your code and pass the error to the nearest catch statement or terminate the program. Rejecting a Promise will invoke the catch callback with whatever value was rejected, but it will not stop the execution of the Promise if there is more code to run unless the call to the reject function was prefixed with the return keyword. This is why the console.log(a + b); statement still fires even after the rejection. To avoid this behavior, simply end the execution early by using return reject(...).

Conclusion

The try-catch statement is a useful tool that you will certainly use in your career as a programmer. However, any method used indiscriminately may not be the best solution. Remember, you need to use the right tools and concepts to solve specific problems. For example, you typically wouldn’t use a try-catch statement when you don’t expect any exceptions to occur. If errors do happen in these cases, you can identify and fix them as they arise.

Meticulous

Meticulous is a tool for software engineers to easily create end-to-end tests. Use our open source CLI to open an instrumented browser which records your actions and translates them into a test. Meticulous makes it easy to integrate these tests into your CI.

Meticulous has an option to automatically mock out all network calls when simulating a recorded sequence of actions. If you use this option, you do not need a backend environment to use Meticulous and Meticulous tests never cause side effects (like affecting analytics) or hit your backend.

Try out our open-source CLI and create your first test in 60 seconds using our docs.

Authored by Rhuan Souza

Read more