Several years ago, the entire booking and check-in system for a major airline in the United States ceased to function for more than an hour during the morning rush on a weekday. This resulted in flight delays for the entire day all across the country. In the end, the cause was found to be an unhanded error that resulted in a flight lookup service to become completely unresponsive.

Handling errors in TypeScript and JavaScript is one of the fundamental things a developer should be experienced in. It’s as important as the rest of the code, and should never be overlooked or underestimated. This is a guide to help newer developers understand how to handle errors, throwing errors, and using try/catch/finally according to industry standards, and also how not to handle them.

Best Practice – Getting Type Information When Catching Errors

Here is a method of getting type information when catching an error:

try {
    // code that may throw an error...
}
catch(e) {
    if(e instanceof Error) {
        // IDE type hinting now available
        // properly handle Error e
    }
    else if(typeof e === 'string' || e instanceof String) {
        // IDE type hinting now available
        // properly handle e or...stop using libraries that throw naked strings
    }
    else if(typeof e === 'number' || e instanceof Number) {
        // IDE type hinting now available
        // properly handle e or...stop using libraries that throw naked numbers
    }
    else if(typeof e === 'boolean' || e instanceof Boolean) {
        // IDE type hinting now available
        // properly handle e or...stop using libraries that throw naked booleans
    }
    else {
        // if we can't figure out what what we are dealing with then
        // probably cannot recover...therefore, rethrow
        // Note to Self: Rethink my life choices and choose better libraries to use.
        throw e;
    }
}

As shown above, attempting to find the type of error you are handling can get quite tedious. In most cases, you will know what you kinds of error situations you can recover from. The rest should be re-thrown and never swallowed (more on this later). Here is a much simplified and more typical version:

try {
    // code that may throw an error...
}
catch(e) {
    if(e instanceof Error) {
        // IDE type hinting now available
        // properly handle Error e
    }
    else {
        // probably cannot recover...therefore, rethrow
        throw e;
    }
}

Best Practice – Always Use Error Objects, Never Scalars

When throwing or returning errors, always use instances of the Error object or objects derived from it. Never throw scalars (e.g. number, naked strings, etc). This ensure stack traces are collected and error typing can be used when handling the error.

Here is an example of returning an Error in code utilizing callbacks:

function example1(param: number, callback: (err: Error, result: number) => void) {
    try {
        // code that may throw an error...
        const exampleResult = 1;
        callback(null, exampleResult);
    }
    catch(e) {
        callback(e, null); // caller handles the error
    }
}

Here is an example of throwing an error in an async function:

async function example2() {
    await functionReturningPromise();
    throw new Error('example error message');
}

Notice how much less code is needed and how it much simpler it appears. This is a common theme when using async/await. I cannot recommend it enough. Back to errors…

Best Practice – Free Resources in Finally

The finally clause should be used to free any resources.

async function example2() {
    let resource: ExampleResource = null;

    try {
        resource = new ExampleResource();
        resource.open();
        // use the resource...
    }
    catch(e) {
        if(e instanceof Error) {
            // IDE type hinting now available
            // handle error if possible
            // be sure to re-throw it if you can't properly resolve it
        }
        else {
            // probably dealing with a naked string or number here
            // handle if you can, otherwise re-throw.
            throw e;
        }
    }
    finally {
        // always executed
        // free resources (e.g. close database) as needed
        if(resource != null) {
            resource.close();
        }
    }
}

Best Practice – Be Specific with Errors

Often you will want to throw or return errors that are more specific than the generic Error object. That is easy to do by extending the standard error object.

class InvalidArgumentError extends Error {
    constructor(message?: string) {
        super(message);
        // see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
        Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
        this.name = InvalidArgumentError.name; // stack traces display correctly now 
    }
}

The goal with having a more specific error is to maximize the chance that the error can be handled locally and allow code execution to continue. Here is how you catch the specific errors.

try {
    throw new InvalidArgumentError();
}
catch(e) {
    if(e instanceof InvalidArgumentError) {
        // handle InvalidArgumentError
    }
    else if(e instanceof Error) {
        // handle generic Errors
        // IMPORTANT: list errors from most specific to least specific (child ~> parent)
    }
    else {
        throw e;
    }
}

Errors within the “if” conditions must be listed from most specific to least specific. After all, anĀ InvalidArgumentError is an instance of an Error too.

Best Practice – Never Swallow Exceptions

As far as the browser is concerned as long as the Error is caught, it will happily continue executing your code. In fact, doing something with the Error in the catch block is entirely optional. Swallowing an exception refers to the act of catching an Error and then doing nothing to fix the problem. Here is an example of swallowing an Error.

function exceptionSwallower1() {
    try {
        throw new Error('my error');
    }
    catch(e) { /* it's our little secret */ }
}

While valid code, it is very bad practice. At a minimum, the Error should be re-thrown.

This is valid and the compiler doesn’t complain, but please don’t ever do this unless you have an extremely well documented reason. While the exception is caught, we do nothing to fix the arising issue and any kind of useful information we could extract from the caught error thrown on the floor and is lost.

Another common and not very helpful practice is to log the Error to the console and continue:

function exceptionSwallower2() {
    try {
        throw new Error('my error');
    }
    catch(e) {
        console.log(err);
    }
}

Errors can also be swallowed by abruptly returning in a finally block:

function exceptionSwallower3() {
    try {
        throw new Error('my error');
    }
    finally {
        return null;
    }
}

First the error is thrown. Then the finally block is executed and the code abruptly returns resulting in all error information being lost. The error is again swallowed.

Errors can also be swallowed by shadowing them:

function exceptionSwallower4() {
    try {
        throw new Error('my error');
    }
    finally {
        throw new Error('different error'); // the first error is now completely hidden
    }
}

Best Practice – Never Use throw as a GoTo

Sometimes, someone will think they are clever by using the try/catch mechanism as way to control code flow when an error condition doesn’t really exist.

function badGoto() {
    try {
        // some code 1

        if(some_condition) {
            throw new Error();
        }
        // some code 2
    }
    catch(e) {
        // some_condition was true
        // some code 3 (non-error handling code)
    }
}

The end goal of this code example is to skip “some code 2”. This is ineffective and slow due to try/catch being designed for error conditions, not regular program logic flow. JavaScript/TypeScript offers more than enough flow control logic to do just about anything, so simulating a goto statement is not the best idea.

Best Practice – Never Catch for the Purpose of Logging and Then Re-throwing

When trying to figure out why your application is crashing, do not both log the exception and then immediate re-throw it:

function clogMyLogs() {
    try {
        throw new Error('my error');
    }
    catch(e) {
        console.log(err);
        throw err;
    }
}

Doing both is redundant and will result in multiple log messages that will clog your logs with the amount of text.