Strongly Typed Error Handling in TypeScript
Tags: typescript, javascript, design, programming
Published: 2021-01-16 14:42:46

TypeScript is a great language. TypeScript takes JavaScript and makes it _actually good._ If there’s one glaring weakness, it’s the [inability to use strongly typed catch blocks](https://github.com/Microsoft/TypeScript/issues/8677). However, this is mostly due to a design flaw in the JavaScript language; in JavaScript, you can `throw` _anything_, not just `Error` types.

Justification
=============

Consider the following, completely valid TypeScript code:
```ts
try {
  const x: number = 7;
  throw x;
} catch (e) {
  console.log(typeof e); // "number"
  console.log(e); // 7
}
```
<figcaption>You can quite literally throw anything.</figcaption>

It’s easy to see why this can be a problem.  One use-case where this is less than ideal is consuming a web API from TypeScript. It’s quite common for non-success HTTP status codes (500 Internal Server Error, 404 Not Found, etc.) to be thrown as an error by API consumer code.

Usage
=====

All models are `Immutable.js` `Record` classes.

```ts
/*
Let's say the API returns a 500 error, with an error
result JSON object:
-- Status: 500
-- Data:
{
  "errors": [
    {
      "key": "UserAlreadyExists",
      "message": "User with specified email already exists.",
      "type": "Validation"
    }
  ]
  "resultObject": undefined
}
*/

const createUserApi = ServiceFactory.create(UserRecord, "/api/users/");

const createUser = async (user: UserRecord): Promise<UserRecord | undefined> => {
  try {
    const result = await createUserApi(user);
    return result.resultObject;
  } catch (e) {
    // if the thing that was thrown is a ResultRecord, show a toast message for each error
    if (e instanceof ResultRecord) {
      e.errors.forEach((error: ResultErrorRecord) => toast.error(error.message));
      return undefined.
    }
  
    // otherwise, it could be anything, so just show a generic error message
    toast.error("There was an issue creating the user.");
    return undefined;
  } finally {
    // maybe we need to turn off a loading indicator here
  }
};
```

You can see in this example that handling errors natively in TypeScript is… quite sloppy. The [“maybe monad”](https://en.wikipedia.org/wiki/Monad_%28functional_programming%29#An_example:_Maybe) common pattern to more generically handle errors and control flow. Basically, what we want to do is create an abstraction that can strongly type thrown errors to a specified type that you know is likely to be thrown. In our case, we want to be able to handle errors from a strongly typed `ResultRecord` with `ResultErrorRecord`s inside it.

What if we could take the example above, and represent the same logic but with less code and strong typing in the catch block? In the following example, one of `result` or `error` will be non-null, but not both.

```ts
/*
Let's say the API returns the same response as before.
result JSON object:
-- Status: 500
-- Data:
{
  "errors": [
    {
      "key": "UserAlreadyExists",
      "message": "User with specified email already exists.",
      "type": "Validation"
    }
  ]
  "resultObject": undefined
}
*/

const createUserApi = ServiceFactory.create(UserRecord, "/api/users/");

const createUser = async (user: UserRecord): Promise<UserRecord | undefined> =>
  await Do.try(async () => {
    const result = await createUserApi(user);
    return result.resultObject;
  }).catch((result?: ResultRecord<UserRecord>, error?: any) => {
    // if result is not null, it will have errors; show toast errors
    result?.errors?.forEach((e: ResultErrorRecord) => toast.error(e.message));
    // otherwise, unknown error; show a generic toast message
    error != null && toast.error("There was an issue creating the user.");
  }).finally(() => {
    // maybe we need to turn off a loading indicator here
  }).getAwaiter();
```
<figcaption>Automatic typecasting? Nice 😎</figcaption>

This pattern gives us a more functional approach to error handling, gives us **strongly typed** errors, and works really, really nicely when used in combination with [React hooks](https://reactjs.org/docs/hooks-intro.html).

```ts
const LoginForm: React.FunctionComponent = () => {
  const { create: createUserApi } = UserService.useCreate();
  const history = useHistory();
  const [user, setUser] = useState(new UserRecord());
  const [errors, setErrors] = useState<Array<ResultErrorRecord>>([]);
  const [loading, setLoading] = useState(false);

  const createUser = useCallback(
    async () => await Do.try(async () => {
      setErrors([]);
      setLoading(true);
      const result = await createUserApi(user);
      history.push(RouteUtils.getUrl(siteMap.myProfile, { userId: result.resultObject!.id! }));
    }).catch((result?: ResultRecord<UserRecord>, error?: any) => {
      setErrors(result?.errors ?? []);
      error != null && toast.error("There was an issue signing up.");
    }).finally(() => setLoading(false))
      .getAwaiter(),
    [createUserApi, user]
  );

  return (
    <div className="c-login-form">
      <InputFormField
        disabled={loading}
        label="Email"
        required={true}
        type={InputTypes.Email}
        value={user.email}
        onChange={e => setUser(user.with({ email: e.target.value })}
        errors={errors.filter(e => e.key === "SignUp.Email")}
      />
      <InputFormField
        disabled={loading}
        label="Password"
        required={true}
        type={InputTypes.Password}
        value={user.password}
        onChange={e => setUser(user.with({ password: e.target.value })}
        errors={errors.filter(e => e.key === "SignUp.Password")}
      />
      <Button onClick={createUser}>Sign Up</Button>
      {loading && (<LoadingSpinner/>)}
    </div>
  );
};
```
<figcaption>Strong typing is better than weak typing.</figcaption>

Clean, concise, and strongly typed error handling in just 46 lines of code, including the UI.

Implementation
==============

So how does this fancy-schmancy `Do.try` work under the hood? By adding an abstraction on top of regular old [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)s. Let’s break it down.

First, let’s define some utility types we’re going to need:

```ts
/**
 * Represents an asynchronous method reference.
 */
type AsyncWorkload<T> = () => Promise<T>;

/**
 * Handler for a typed error ResultRecord, or any type if a Javascript error occurred.
 */
type CatchResultHandler<T> = (result?: ResultRecord<T>, error?: any) => void;

/**
 * Handler for Do.try().finally(); Runs whether an error occurred or not.
 */
type FinallyHandler = () => void;
```
<figcaption>Strongly typed handlers for strongly typed errors.</figcaption>

Next, let’s take a look at our constructor:

```ts
class Do<TResourceType, TReturnVal = void> {
  private promise: Promise<TReturnVal>;
  
  private constructor(workload: AsyncWorkload<TReturnVal>) {
    this.promise = workload();
  }
}
```
<figcaption>Private constructor? 🤔</figcaption>

That `private constructor` is no mistake. You’ll notice in the previous snippets, usage of this pattern starts with `Do.try`; that’s because `try` is a static factory method that returns an instance of `Do`. The `private constructor` can only be called internally to the class, by the `try` method. The implementation of `try` is very straightforward:

```ts
class Do<TResourceType, TReturnVal = void> {
  ... 
  public static try<TResourceType, TReturnVal = void>(
    workload: AsyncWorkload<TReturnVal>
  ): Do<TResourceType, TReturnVal> {
    return new Do<TResourceType, TReturnVal>(workload);
  }
}
```
<figcaption>You’ll never know unless you… try.</figcaption>

The `finally` method is just as straightforward, with one important caveat:

```ts
class Do<TResourceType, TReturnVal = void> {
  ...
  public finally(
    finallyHandler: FinallyHandler
  ): Do<TResourceType, TReturnVal> {
    this.promise = this.promise.finally(finallyHandler);
    return this;
  }
}
```
<figcaption>Pay special attention to the return value.</figcaption>

Notice the return value, `return this;` This allows for method chaining, i.e. `Do.try(workload).catch(catchHandler).finally(finallyHandler);` In this code, `catch` and `finally` are both called on the same instance of `Do` which is returned from `Do.try`.

There’s also a `getAwaiter` method, which allows us to `await` for the result. All we need to do is return the internal promise.

```ts
class Do<TResourceType, TReturnVal = void> {
  ...
  public async getAwaiter(): Promise<TReturnVal> {
    return this.promise;
  }
}
```
<figcaption>This just allows us to await the result of the entire method chain.</figcaption>

Now let’s get to the interesting part; the `catch` method. Inside the catch method, we’re going to [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) the thrown object; if the thrown object is a `ResultRecord` instance, we cast it as such and pass it as the catch handler’s first argument; otherwise, it’s some unknown error, so we pass it as the catch handler’s second argument. We also need to cast the promise back to a `Promise<TReturnVal>` because of the return type of `Promise.catch`, but the promise is still a valid `Promise<TReturnVal>`.

```ts
class Do<TResourceType, TReturnVal = void> {
  public catch(
    errorHandler: CatchResultHandler<TResourceType>
  ): Do<TResourceType, TReturnVal> {
    this.promise = this.promise.catch((err: any) => {
      // check if thrown object is a ResultRecord
      if (err instanceof ResultRecord) {
        // pass the ResultRecord as the first parameter
        errorHandler(err, undefined);
        return;
      }
      
      // otherwise, pass the generic error as the second parameter
      errorHandler(undefined, err);
    }) as Promise<TReturnVal>;
    
    // notice again, we are returning this to allow method chaining
    return this;
  }
}
```
<figcaption>We just need to cast the promise back to a `Promise<TReturnVal>` because of the return type of `Promise.catch`, but rest assured, the promise is still a valid `Promise<TReturnVal>`.</figcaption>

And there you have a basic implementation of a “maybe monad”. While the implementation here is an opinionated one, offering strongly typed error handling for `ResultRecord` errors, you could easily implement the same thing for virtually any type you want to use to wrap up your errors, just as long as you’re able to implement a type guard for it.

Taking It Further
=================

I think strongly typed error handling speaks enough for itself, but we can take it even further. This pattern enables an extremely powerful utility, and I think it’s the strongest argument for using it: [_default behavior_](https://en.wikipedia.org/wiki/Default_(computer_science)#In_application_software). We can extend our `Do` class to have a global configuration, allowing us to define _default behavior which is applied to every instance of `Do` across the entire application_.

All we need to do is add a static configuration mechanism, and implement a check for our configuration inside the constructor:

```ts
interface DoTryConfig {
  /**
   * A default handler that will always run on error, if configured,
   * even if a `catch()` does not exist in the call chain.
   * This is useful for adding default error handling in the
   * development environment, such as `console.error(err)`.
   */
  defaultErrorHandler?: CatchResultHandler<any>;
}

class Do<TResourceType, TReturnVal = void> {
  ...
  private static config: DoTryConfig = {
    defaultErrorHandler: undefined,
  };
  
  private constructor(workload: AsyncWorkload<TReturnVal>) {
    this.promise = workload().catch((err: any) => {
      // check for defaultErrorHandler from config
      if (err instanceof ResultRecord) {
        Do.config.defaultErrorHandler?.(err, undefined);
        throw err; // rethrow so it doesn't interrupt call chain
      }

      Do.config.defaultErrorHandler?.(undefined, err);
      throw err; // rethrow so it doesn't interrupt call chain
    });
  }
  
  /**
   * Sets the global configuration object for class {Do}
   * @param config the {DoTryConfig} object to set
   */
  public static configure(config: DoTryConfig): void {
    Do.config = config;
  }
}
```
<figcaption>Default behavior is epic.</figcaption>

So what does it look like to apply default behavior? Let’s contrive an example.

We’re working on a large scale React application, and in order to aid debugging errors during development, we want to always log errors to the console in the development environment. Well, with the configuration mechanism we just added, it becomes trivially easy to add this default behavior. Just open up your `index.ts` app entrypoint and add the handler:

```ts
// index.ts

EnvironmentUtils.runIfDevelopment(() => {
  Do.configure({
    defaultErrorHandler: (result?: ResultRecord<any>, error?: any) => {
      result != null && console.error(result);
      error != null && console.error(error);
    }
  });
});
```
<figcaption>Log errors to the console by default.</figcaption>

You could use the same configuration mechanism to add default behavior to the `try` or `finally` portions of the call chain as well.

The syntax is quite nice to read and easy to understand at a glace, but with the added bonus of having strongly typed errors, and optional default behavior.

What do you think? Are you going to try “maybe monads” or the `Do.try` pattern in your next TypeScript project?