Execute Program

Everyday TypeScript: Exceptions

Welcome to the Exceptions lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We've seen many places where TypeScript checks our types: variables, function arguments, object properties, etc. In this lesson, we'll explore one place where TypeScript can't help us much: exceptions.

  • The code below intentionally throws an Error. We try to catch this error with the TypeScript Error type, but it doesn't work. In fact, our attempt to catch the Error is itself a type error!

  • >
    try {
    throw new Error('something failed');
    } catch (e: Error) {
    console.log('we caught the exception');
    }
    Result:
    type error: Catch clause variable type annotation must be 'any' or 'unknown' if specified.Pass Icon
  • We can never say catch (e: Error). As the error message says, we can only catch (e: any) or catch (e: unknown).

  • When using TypeScript, we need to work around this limitation. The type error message above spells out two choices for navigating this.

  • The first choice is to catch any. As always, we lose type safety if we use any.

  • JavaScript and TypeScript allow us to throw any value, not just instances of Error. We can throw a string, or a number, or null, or anything else. If we catch (e: any), then the actual values at runtime may not match the static types.

  • >
    let error: Error;
    try {
    throw 'this is not an error object';
    } catch (e: any) {
    error = e;
    }
    error;
    Result:
    'this is not an error object'Pass Icon
  • The types there were wrong: our variable was supposed to hold an Error, but it held a string. Using any here (or almost anywhere else!) risks introducing this kind of bug.

  • Our other type option when catching is unknown. This is safer because it forces us to narrow the type with type guards.

  • The next two examples are similar, but only one of them uses a type guard on the caught exception. The type guard version works. Without a type guard, we get a type error.

  • >
    let error: Error;
    try {
    throw new Error('something broke');
    } catch (e: unknown) {
    error = e;
    }
    error;
    Result:
    type error: Type 'unknown' is not assignable to type 'Error'.Pass Icon
  • >
    let error: Error;

    try {
    throw new Error('something broke');
    } catch (e: unknown) {
    /* We can only catch exceptions as `unknown` or `any`. We use `unknown`
    * because it's safer, but then we have to narrow the type with a
    * conditional. */
    if (e instanceof Error) {
    error = e;
    } else {
    throw new Error("We can't handle that type of exception!");
    }
    }
    error.message;
    Result:
    'something broke'Pass Icon
  • That example shows our recommended solution for exceptions in TypeScript. We recommend catching the error as catch (e: unknown), then using e instanceof OurErrorClass as a type guard. Fortunately, this works well with custom error classes, as long as we make sure to inherit from Error.

  • Here's a code problem:

    The code below catches an exception and tries to store it in the error variable with the type UserDoesNotExistError. Currently, it causes a type error, because we can't assign an unknown to a UserDoesNotExistError. Add an if around the error = e assignment, acting as a type guard.

    You can check for whether the error is an instance of UserDoesNotExistError with e instanceof UserDoesNotExistError. Make sure to add an else clause that throws when the type guard returns false. Otherwise, you'll probably get a type error saying that error was "used before being assigned".

    class UserDoesNotExistError extends Error {
    userId: number;
    constructor(userId: number) {
    super();
    this.userId = userId;
    }
    }

    let error: UserDoesNotExistError;
    try {
    throw new UserDoesNotExistError(57);
    } catch (e: unknown) {
    if (e instanceof UserDoesNotExistError) {
    error = e;
    } else {
    throw new Error("We can't handle that type of exception!");
    }
    }
    error.userId;
    Goal:
    57
    Yours:
    57Pass Icon
  • This course has been full of good news: lesson after lesson, we've shown ways to add static types that prevent bugs before they happen. Unfortunately, this lesson was mostly bad news. TypeScript isn't good at statically typing exceptions, but we can work around that with type guards like instanceof. Fortunately, catch is relatively uncommon, so this is only a mild inconvenience in practice.