Mimicing Rust's Result type in typescript
Tags: typescript, webdev
Published: 2020-11-02 11:55:16

# In the beginning...

...of every project there are API calls without error handling. One of ours looked like this:

```typescript
  /**
   * Register a new (local) account.
   *
   * @param email Email of the new user.
   * @param password Password of the new user.
   *
   * @returns Promise of boolean about the success of the registration request.
   */
  public async registerLocalAccount(
    email: string,
    password: string
  ): Promise<boolean> {
    return this._fetch('/auth/local/register', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    }).then(res => res.ok);
  }
```

The problem here is that we don't have any information on failures. We're just checking if the response status code is 200 or not. On failures, the user is faced with message like "Oops, something went wrong. Try again.". Surely we can do better, can't we? Yes! Lets get our selves a `Result<T, E>` type.

# And the Result<T, E> is...

```typescript
export type Result<T, E> = Ok<T, E> | Err<T, E>;

export class Ok<T, E> {
  public constructor(public readonly value: T) {}

  public isOk(): this is Ok<T, E> {
    return true;
  }

  public isErr(): this is Err<T, E> {
    return false;
  }
}

export class Err<T, E> {
  public constructor(public readonly error: E) {}

  public isOk(): this is Ok<T, E> {
    return false;
  }

  public isErr(): this is Err<T, E> {
    return true;
  }
}

/**
 * Construct a new Ok result value.
 */
export const ok = <T, E>(value: T): Ok<T, E> => new Ok(value);

/**
 * Construct a new Err result value.
 */
export const err = <T, E>(error: E): Err<T, E> => new Err(error);
```

Note that this is a _really_ plain version of the type. Inspiration for this is taken from various implementations found online, one of these being [neverthrow](https://github.com/supermacro/neverthrow). Neverthrow also implements some of the utility functions like `map`.

A few notes:

1. We have a union type `Result<T, E>`, which is either `Ok` or `Err`
2. `Ok` and `Err` types have `isOk` and `isErr` type guards which can be used to tell which ever the values is
3. We have `ok` and `err` constructors which are used to construct a new `Result` type

Now, simple usage of this might look something like this:

```typescript
function getCount(): Promise<Result<number, Error>> {
  return fetch('/index-count')
    .then(res => res.json())
    .then((body): Ok<number, Error> => ok(body['count']))
    .catch(() => err(new Error('Something when wrong while fetching count')));
}

const res: Result<number, Error> = await getCount();

// To access the count, we'll first have to check if the calculation succeeded.
if (res.isOk()) {
  // Now we can access the value.
  console.log('Count is:', res.value);
}

if (res.isErr()) {
 // Now we can access the error.
 console.error('Oh no, there was an error:', res.error)
}
```

Cool, that should be enough to get you started with fancy and elegant error handling. But wait, there's more! Heres some more meat around the bones of the `Result` type.

# More on the network errors

In our code, we've defined a common `NetworkError` type for all our API return types. It looks like this:

```typescript
export type NetworkError<T> = {
  // Was this error expected or not?
  unexpected: boolean;
  // Possible value for the error.
  value?: T;
};

// Utility function to construct a new NetworkError.
export const networkError = <T>(
  unexpected: boolean,
  value?: T
): NetworkError<T> => ({ unexpected, value });
```

Here, we wrap any possible (known) errors into an object with a field that also tells us if the error was unexpected or not. Unexpected errors might be network failures or internal server errors, while expected errors usually are input validation errors, like a bad email or a weak password.

Suddenly, our API request handler looks like this:

```typescript
// Error codes used by the backend.
export enum ApiErrorCode {
  PasswordTooWeak = 1,
  EmailInvalid = 2,
  EmailAlreadyInUse = 3
}

export type RegisterLocalAccountError = {
  email?: ApiErrorCode.EmailInvalid | ApiErrorCode.EmailAlreadyInUse;
  password?: ApiErrorCode.PasswordTooWeak;
};

export type RegisterLocalAccount = Result<
  boolean,
  NetworkError<RegisterLocalAccountError>
>;

public async registerLocalAccount(
  email: string,
  password: string
): Promise<RegisterLocalAccount> {
  return this._fetch('/auth/local/register', {
    method: 'POST',
    body: JSON.stringify({ email, password })
  })
    .then(
      async (res): Promise<RegisterLocalAccount> => {
        if (res.ok) {
          return ok(true);
        }

        if (res.status === 400) {
          const e = await res
            .json()
            .then(
              (reason: RegisterLocalAccountError): RegisterLocalAccount =>
                err(networkError(false, reason))
            )
            .catch((): RegisterLocalAccount => err(networkError(true)));

          return e;
        }

        return err(networkError(true));
      }
    )
    .catch(() => err(networkError(true)));
}
```

_Ugh, more types_ you might say. Trust me, there is a reason for those: in typescript you can do an exhaustive switch statement. For example, our email error message utility function looks like this:

```typescript
const emailError = (
  error: NetworkError<RegisterLocalAccountError> | null
): string => {
  if (
    error === null ||
    error.value === undefined ||
    error.value.email === undefined
  ) {
    return '';
  }

  switch (error.value.email) {
    case ApiErrorCode.EmailAlreadyInUse:
      return 'Email is already in use';
    case ApiErrorCode.EmailInvalid:
      return 'Invalid email';
  }
};
```

Now, if we were to extend the `RegisterLocalAccountError` to this:

```typescript
export type RegisterLocalAccountError = {
  email?: ApiErrorCode.EmailInvalid | ApiErrorCode.EmailAlreadyInUse | 'random error';
  password?: ApiErrorCode.PasswordTooWeak;
};
```

The compiler would yell at us:

```
Function lacks ending return statement and return type does not include 'undefined'.  TS2366

    91 | const emailError = (
    92 |   error: NetworkError<RegisterLocalAccountError> | null
  > 93 | ): string => {
       |    ^
    94 |   if (
    95 |     error === null ||
    96 |     error.value === undefined ||
```

Even though the error message isn't ideal (it's not telling which case we're not handling), it's still 100x better than a runtime error (or no error at all!).

Anyway, lets take a bit closer look at the API handler code. There are three return points on the `then` handler:

1. The happy path, which returns `Ok` if the response status code is 200
2. First error path when we get an "expected" 400 status code. On this path, we should get a JSON body telling us about validation errors. So if our JSON parsing succeeds, we'll return a network error that is "expected". But if the JSON parsing fails, we'll return a network error that is _unexpected_.
3. If the status code is not 200 nor 400, we'll just return a network error that is unexpected.

All the error values are wrapped into a `Result<T, E>` with the `err` constructor. Also note the final `catch`, in which we return a network error which is (again) unexpected. This last catch is extremely important because we don't want to pollute our caller code with try/catch. Instead, we'll want to rely on the `Result` type.


# Conclusion

Depending on your taste on the topic (and mainly how much you like static types and are familiar with functional types), the `Result<T, E>` type can be extremely helpful. We've tried to extract the most out of typescript to help us with development and with always shipping functioning software. The result type is part of our efforts to model our API inputs and outputs into the type system. When done properly, we can fearlessly make changes to our backend and just take care that we update our frontend types that are describing the backend. At that point, the compiler will work with you and tell the places where you might need to extend your error handling and so on.

One obvious downside with the result type given earlier is that it's an class and not a plain object. You might want to turn the classes into a plain serializable objects for your redux store.