DEV Community

Gio
Gio

Posted on

3 2

Chaining Failable Tasks

originally posted on my blog

This post assumes familiarity with TypeScript.

In my previous post, Type-Safe Error Handling In TypeScript, 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):

  • The package introduces a functional alternative to throwing exceptions
    • By getting rid of throwing exceptions, you make your error handling logic pure!
    • This is the standard approach in many other languages such as Rust, Elm and Haskell to name a few. This isn't some random wild experiment I invented.
  • neverthrow has a Result type that represents either success (Ok) or failure (Err)

Result is defined as follows:

type  Result<T, E>
  =  Ok<T, E>
  |  Err<T, E>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  • getUserFromSessionId
  • getCatsByUserId
  • getCatFavoriteFoodsByCatIds

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

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

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:

  • First call getUserFromSession
  • then get the User and use that value to call getCatsByUserId
  • then get all of the cats (Cat[]) and call getCatFavoriteFoodsByCatIds by passing it an array of cat ids

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))
Enter fullscreen mode Exit fullscreen mode

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)
  }
)
Enter fullscreen mode Exit fullscreen mode

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
    ))
  )
)
Enter fullscreen mode Exit fullscreen mode

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

  • chain: takes 2 async Result tasks
  • chain3: takes 3 async Result tasks
  • chain4: takes 4 async Result tasks
  • chain5: etc
  • chain6: etc
  • chain7: etc
  • chain8: etc

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!

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay