Prefer .then() over .catch()

When designing asynchronous APIs that could error in Flow, prefer using .then for both successful and failure cases. Flow exposes a relatively unsafe library definition for the .catch method, so it’s best to avoid it if you can.

Problem

What does this look like in practice? Say you’re thinking about writing code that looks similar to this:

// "Success" and "failure" types (definitions omitted)
import type {OkResult, ErrResult} from 'src/types';

const doSomething = (): Promise<OkResult> => {
  return new Promise((resolve, reject) => {
    // call resolve(...) when it worked, but
    // cal reject(...) when it failed.
  });
};

doSomething
  .then((res) => ...)
  .catch((err) => ...)
Bad code; don’t do this

This is okay code, but not great. Why? Because Flow won’t prevent us from calling reject(...) with something that’s not of type ErrResult, and it won’t warn us when we try to use err incorrectly. Concretely, if we had this type definition:

type ErrResult = string;

Flow wouldn’t prevent us from doing this:

// number, not a string!
reject(42);

nor from doing this:

// boolean, not a string!
.catch((err: boolean) => ...);

Solution

As mentioned, we can work around this by only using resolve and .then. For example, we can replace our code above with this:

// Helper function for exhaustiveness.
// See here: https://blog.jez.io/flow-exhaustiveness/
import {absurd} from 'src/absurd';

import type {OkResult, ErrResult} from 'src/types';

// Use a union type to mean "success OR failure"
type Result =
  | {|tag: 'ok', val: OkResult|}
  | {|tag: 'err', val: ErrResult|};

//     Use our new union type ──┐
const doSomething = (): Promise<Result> => {
  return new Promise((resolve, reject) => {
    // call resolve({tag: 'ok',  val: ...}) when it worked, and
    // call resolve({tag: 'err', val: ...}) when it failed
  });
};

doSomething
  // Use a switch statement in the result:
  .then((res) => {
    switch (res.tag) {
      case 'ok':
        // ...
        break;
      case 'err':
        // ...
        break;
      default:
        // Guarantees we covered all cases.
        absurd(res);
        break;
    }
  })
Better code than before

There’s a lot of benefits in this newer code:

Caveats

Of course, there are some times when the you’re interfacing with code not under your control that exposes critical functionality over .catch. In these cases, it’s not an option to just “not use .catch”. Instead, you have two options.

If you trust that the library you’re using will never “misbehave”, you can ascribe a narrow type to the .catch callback function:

// If I know that throwNumber will always call `reject` with a
// number, I can stop the loose types from propagating further
// with an explicit annotation:
throwNumber
  .then(() => console.log("Didn't throw!"))
  //         ┌── explicit annotation
  .catch((n: number) => handleWhenRejected(n))

If you aren’t comfortable putting this much trust in the API, you should instead ascribe mixed to the result of the callback.

throwNumber
  .then(() => console.log("Didn't throw!"))
  //         ┌── defensive annotation
  .catch((n: mixed) => {
    if (typeof n === 'number') {
      handleWhenRejected(n);
    } else {
      // Reports misbehavior to an imaginary observability service
      tracker.increment('throwNumber.unexpected_input');
    }
  });