Giorgio Delgado

Chaining Failable Tasks

March 7, 2020

This post assumes familiarity with TypeScript.

In my previous post, I introduced a npm package to model failure at the type level.

If you're not familiar with neverthrow, here's a quick rundown (feel free to skip this tiny intro by clicking here):

Result is defined as follows:

type  Result<T, E>
  =  Ok<T, E>
  |  Err<T, E>

Ok<T, E>: contains the success value of type T

Err<T, E>: contains the failure value of type E

Usage:

Create Ok or Err instances with the ok and err functions.

import { ok, err } from 'neverthrow'

// something awesome happend

const yesss = ok(someAwesomeValue)

// moments later ...

const mappedYes = yesss.map(doingSuperUsefulStuff)

You can access the value inside of Err and Ok instances as follows:

if (myResult.isOk()) {
  // if I didn't first call `isOk`, I would get a compilation error
  myResult.value
}

// or accessing values
if (myResult.isErr()) {
  myResult.error
}

This quick rundown doesn't do the package justice, so I highly recommend you check out my previous post that really walks you through the package.


A while back, I got feedback (link to github issue) from two users that this module wasn't very ergonomic when it came to Results wrapped inside of a promise.

This post is dedicated to covering the problem, and the solution to it.

The Problem

Let's suppose we're working on a project that has 3 async functions:

And here are the type signatures for each of these functions:

type GetUserFromSessionId = (sessionUUID: string) => Promise<Result<User, AppError>>
type GetCatsByUserId = (userId: number) => Promise<Result<Cat[], AppError>>
type GetCatFavoriteFoodsByCatIds = (catIds: number[]) => Promise<Result<Food[], AppError>>

Let's also assume that you're a developer tasked with leveraging these functions in order to get all of the favorite foods of all of the cats owned by a single user.

By taking a close look at the type signatures of these functions, we can start to see how we might go about implementing our task:

The issue is that the values we need (User, Cat[] and Food[]) are wrapped inside of Promise and Result.

First Attempt At A Solution

Let's see how we might implement this naively.

The neverthrow api has a asyncMap method and andThen method that we could use to solve this:

// imagine we have a sessionId already

const result1 = await getUserFromSessionId(sessionId)

// result2 is a Result<Result<Cat[]>, AppError>, AppError>
const result2 = await result1.asyncMap((user) => getCatsByUserId(user.id))

// need to get the inner result using `andThen`
// now catListResult is Result<Cat[]>, AppError>
const catListResult = result2.andThen((innerResult) => innerResult)

// result3 is
// Result<Result<Food[], AppError>, AppError>
const result3 = await catListResult.asyncMap(
  (cats) => getCatFavoriteFoodsByCatIds(cats.map((cat) => cat.id))
)

// so now we need to unwrap the inner result again ...
// foodListResult is Result<Food[], AppError>
const foodListResult = result3.andThen((innerResult => innerResult))

Holy boilerplate! That was not fun. And super cumbersome! There was a lot of legwork required to continue this chain of async Result tasks.

... If there were only a better way!

Using Result Chains! 🔗

Version 2.2.0 of neverthrow introduces a wayyy better approach to dealing with this issue.

This is what it would look like

import { chain3 } from 'neverthrow'

// foodListResult is Result<Food[], AppError>
const foodListResult = chain3(
  getUserFromSessionId(sessionId),
  (user) => getCatsByUserId(user.id),
  (cats) => {
    const catIds = cats.map((cat) => cat.id)
    return getCatFavoriteFoodsByCatIds(catIds)
  }
)

That's it.

Check out the API docs here.

Obviously the above example is quite contrived, but I promise you that this has very practical implications. As an example, here's a snippet from my own side project where I use the chain3 function:

chain3(
  validateAdmin(parsed.username, parsed.password),
  async (admin) => {
    const sessionResult = await session.createSession(admin)

    return sessionResult.map((sessionToken) => {
      return {
        sessionToken,
        admin
      }
    })
  },
  ({ sessionToken, admin }) => Promise.resolve(
    ok(AppData.init(
      removePassword(admin),
      sessionToken
    ))
  )
)

There are 8 different chain functions, each of which only vary in their arity (the number of arguments that the functions take).

The beautiful thing about this chain API is that it retains the same properties as synchronous Result.map chains ... namely, these async chains short-circuit whenever something at the top of the chain results in a Err value 😍

A useful way to think of the chain api is to think of it as the asynchronous alternative to the andThen method.


I've had this issue noodling in my head for a while. Eventually in that same github issue I mentioned at the top of this post, I proposed an approach to chaining many async computations with a set of utility functions.

Before committing to that solution, I started dogfooding this approach through my own side project. After a few days of using this chain API, I concluded that it was in fact quite good and ergonomic.

This API is heavily tested and well-documented!

Cheers!

Liked this post? Consider buying me a coffee :)