Like any programs, JavaScript will encounter error situations, for example, like when JSON fails to parse, or null value is encounter unexpectedly in a variable. This means that we have to handle those errors gracefully if we want our app to give users a good user experience. This means that we have to handle those errors gracefully. Errors often come in the form of exceptions, so we have to handle those gracefully. To handle them, we have to use the try...catch
statement to handle these errors so they do not crash the program.
Try…Catch
To use the try...catch
block, we have to use the following syntax:
try{
// code that we run that may raise exceptions
// one or more lines is required in this block
}
catch (error){
// handle error here
// optional if finally block is present
}
finally {
// optional code that run either
// when try or catch block is finished
}
For example, we can write the following code to catch exceptions:
try {
undefined.prop
} catch (error) {
console.log(error);
}
In the code above, we were trying to get a property from undefined
, which is obviously not allowed, so an exception is thrown. In the catch
block, we catch the ‘TypeError: Cannot read property ‘prop’ of undefined’ that’s caused by running undefined.prop
and log the output of the exception. So we get the error message outputted instead of crashing the program.
The try...catch
statement has a try
block. The try
block must have at least one statement inside and curly braces must always be used, event for single statements. Then either the catch
clause or finally
clause can be included. This means that we can have:
try {
...
}
catch {
...
}
try {
...
}
finally{
...
}
try {
...
}
catch {
...
}
finally {
...
}
The catch
clause has the code that specifies what to do when an exception is thrown in the try
block. If they try
block didn’t succeed and an exception is thrown, then the code in the catch
block will be ran. If all the code in the try
block is ran without any exception thrown, then the code in the catch
block is skipped.
The finally
block executes after all the code the try
block or the catch
block finishes running. It always runs regardless if exceptions are thrown or not.
try
blocks can be nested within each other. If the inner try
block didn’t catch the exception and the outer one has a catch
block, then the outer one will catch the exception thrown in the inner try
block. For example, if we have:
try {
try {
undefined.prop
} finally {
console.log('Inner finally block runs');
}
} catch (error) {
console.log('Outer catch block caught:', error);
}
If we run the code above, we should see ‘Inner finally block runs’ and ‘Outer catch block caught: TypeError: Cannot read property ‘prop’ of undefined’ logged, which is what we expect since the inner try
block didn’t catch the exception with a catch
block so the outer catch
block did. As we can see the inner finally block ran before the outer catch block. try...catch...finally
runs sequentially, so the code that’s added earlier will run before the ones that are added later.
The catch
block that we wrote so far are all unconditional. That means that they catch any exceptions that were thrown. The error
object holds the data about the exception thrown. It only holds the data inside the catch
block. If we want to keep the data outside it then we have to assign it to a variable outside the catch
block. After the catch
block finishes running, the error
object is no longer available.
The finally
clause contains statements that are excepted after the code in the try
block or the catch
block executes, but before the statements executed below the try...catch...finally
block. It’s executed regardless whether an exception was thrown. If an exception is thrown, then statements in the finally
block is executed even if no catch
block catches and handles the exception.
Therefore, the finally
block is handy for making our program fail gracefully when an error occurs. For example, we can put cleanup code that runs no matter is an exception is thrown or not, like for close file reading handles. The remaining code in a try
block doesn’t executed when an exception is thrown when running a line in the try
block, so if we were excepted to close file handles in the try
and an exception is thrown before the line that closes the file handle is ran, then to end the program gracefully, we should do that in the finally
block instead to make sure that file handles always get cleaned up. We can just put code that runs regardless of whether an exception is thrown like cleanup code in the finally
block so that we don’t have to duplicate them in the try
and catch
blocks. For example, we can write:
openFile();
try {
// tie up a resource
writeFile(data);
}
finally {
closeFile();
// always close the resource
}
In the code above, the closeFile
function always run regardless of whether an exception is thrown when the writeFile
is run, eliminating duplicate code.
We can have nested try
blocks, like in the following code:
try {
try {
throw new Error('error');
}
finally {
console.log('finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
If we look at the console log, we should see that ‘finally runs’ comes before ‘exception caught error.’ This is because everything in the try...catch
block is ran line by line even if it’s nested. If we have more nesting like in the following code:
try {
try {
throw new Error('error');
}
finally {
console.log('first finally runs');
}
try {
throw new Error('error2');
}
finally {
console.log('second finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
We see that we get the same console log output as before. This is because the first inner try
block didn’t catch the exception, so the exception is propagated to and caught by the outer catch
block. If we want to second try
block to run, then we have to add a catch
block to the first try
block, like in the following example:
try {
try {
throw new Error('error');
}
catch {
console.log('first catch block runs');
}
finally {
console.log('first finally runs');
}
try {
throw new Error('error2');
}
finally {
console.log('second finally runs');
}
}
catch (ex) {
console.error('exception caught', ex.message);
}
Now we see the following message logged in order: ‘first catch block runs’, ‘first finally runs’, ‘second finally runs’, ‘exception caught error2’. This is because the first try
block has a catch
block, so the the exception caused by the throw new Error('error')
line is now caught in the catch
block of the first inner try
block. Now the second inner try
block don’t have an associated catch
block, so error2
will be caught by the outer catch
block.
We can also rethrow errors that were caught in the catch
block. For example, we can write the following code to do that:
try {
try {
throw new Error('error');
} catch (error) {
console.error('error', error.message);
throw error;
} finally {
console.log('finally block is run');
}
} catch (error) {
console.error('outer catch block caught', error.message);
}
As we can see, if we ran the code above, then we get the following logged in order: ‘error error’, ‘finally block is run’, and ‘outer catch block caught error’. This is because the inner catch
block logged the exception thrown by throw new Error(‘error’)
, but then after console.error(‘error’, error.message);
is ran, we ran throw error;
to throw the exception again. Then the inner finally
block is run and then the rethrown exception is caught by the outer catch
block which logged the error
that was rethrown by the throw error
statement in the inner catch
block.
Since the code runs sequentially, we can run return
statements at the end of a try
block. For example, if we want to parse a JSON string into an object we we want to return an empty object if there’s an error parsing the string passed in, for example, when the string passed in isn’t a valid JSON string, then we can write the following code:
const parseJSON = (str) => {
try {
return JSON.parse(str);
}
catch {
return {};
}
}
In the code above, we run JSON.parse
to parse the string and if it’s not valid JSON, then an exception will be thrown. If an exception is thrown, then the catch
clause will be invokes to return an empty object. If JSON.parse
successfully runs then the parsed JSON object will be returned. So if we run:
console.log(parseJSON(undefined));
console.log(parseJSON('{"a": 1}'))
Then we get an empty object on the first line and we get {a: 1}
on the second line.
Try Block in Asynchronous Code
With async
and await
, we can shorten promise code. Before async
and await
, we have to use the then
function, we make to put callback functions as an argument of all of our then
functions. This makes the code long is we have lots of promises. Instead, we can use the async
and await
syntax to replace the then
and its associated callbacks as follows. Using the async
and await
syntax for chaining promises, we can also use try
and catch
blocks to catch rejected promises and handle rejected promises gracefully. For example , if we want to catch promise rejections with a catch
block, we can do the following:
(async () => {
try {
await new Promise((resolve, reject) => {
reject('error')
})
} catch (error) {
console.log(error);
}
})();
In the code above, since we rejected the promise that we defined in the try
block, the catch
block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. Even though it looks a regular try...catch
block, it’s not, since this is an async
function. An async
function only returns promises, so we can’t return anything other than promises in the try...catch
block. The catch
block in an async
function is just a shorthand for the catch
function which is chained to the then function. So the code above is actually the same as:
(() => {
new Promise((resolve, reject) => {
reject('error')
})
.catch(error => console.log(error))
})()
We see that we get the same console log output as the async
function above when it’s run.
The finally
block also works with the try...catch
block in an async
function. For example, we can write:
(async () => {
try {
await new Promise((resolve, reject) => {
reject('error')
})
} catch (error) {
console.log(error);
} finally {
console.log('finally is run');
}
})();
In the code above, since we rejected the promise that we defined in the try
block, the catch
block caught the promise rejection and logged the error. So we should see ‘error’ logged when we run the code above. The the finally
block runs so that we get ‘finally is run’ logged. The finally
block in an async
function is the same as chaining the finally
function to the end of a promise so the code above is equivalent to:
(() => {
new Promise((resolve, reject) => {
reject('error')
})
.catch(error => console.log(error))
.finally(() => console.log('finally is run'))
})()
We see that we get the same console log output as the async
function above when it’s run.
The rules for nested try...catch
we mentioned above still applies to async
function, so we can write something like:
(async () => {
try {
await new Promise((resolve, reject) => {
reject('outer error')
})
try {
await new Promise((resolve, reject) => {
reject('inner error')
})
} catch (error) {
console.log(error);
} finally {
}
} catch (error) {
console.log(error);
} finally {
console.log('finally is run');
}
})();
This lets us easily nest promises and and handle their errors accordingly. This is cleaner than chaining the then
, catch
and finally
functions that we did before we have async
functions.
To handle errors in JavaScript programs, we can use the try...catch...finally
blocks to catch errors. This can be done with synchronous or asynchronous code. We put the code that may throw exceptions in the try
block, then put the code that handles the exceptions in the catch
block. In the finally
block we run any code that runs regardless of whether an exception is thrown. async
functions can also use the try...catch
block, but they only return promises like any other async
function, but try...catch...finally
blocks in normal functions can return anything.