JavaScript Try Catch | Complete Guide & Tutorial
A guide on the try-catch functionality available in JavaScript, it's quirks and tips on using this most effectively.

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.
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 indecodeURI()
orencodeURI()
. -
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 catch visual regressions in web applications without writing or maintaining UI tests.
Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.
Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.
Learn more here.
Authored by Rhuan Souza