Prefer .then() over .catch()

May 31, 2018

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.


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

Bad code; don’t do 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.

  .then((res) => ...)
  .catch((err) => ...)

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!

nor from doing this:

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


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

Better code than before
// Helper function for exhaustiveness.
// See here:
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

  // Use a switch statement in the result:
  .then((res) => {
    switch (res.tag) {
      case 'ok':
        // ...
      case 'err':
        // ...
        // Guarantees we covered all cases.

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


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:
  .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.

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

Concurrent Programming in ML: A Race

I want to call attention to what I think is a race condition in one of the code listings in the book “Concurrent Programming in ML”. The problem is that some of the data isn’t protected by a lock, which can lead to a stale read and incorrect behavior. I trace the bad behavior, and propose a fix. Continue reading

Union Types in Flow & Reason

Published on April 19, 2018

Case Exhaustiveness in Flow

Published on April 15, 2018