Categories
JavaScript Basics

Handling Exceptions in JavaScript

Spread the love

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.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *