Chaining Async Functions like a Boss

Chaining asynchronous functions is a common problem across languages. JavaScript has Promise to deal with this, Elm has Task, and the list goes on. However, many real-world problems are more complex than straightforward linear chaining. To solve this, many languages have introduced special syntactic sugar over their chaining API.

Pseudocode

We want to measure how long an HTTP request takes. To do so, we check the time before and after the request completes and calculate the difference. In pseudocode we could say:

get the current time
THEN get the user data via HTTP
THEN get the current time
THEN calculate the difference between the start time
     and end time

Elm

In Elm, both HTTP requests and getting the current time are async operations. We can chain them with Task.andThen. However, things are not as straightforward as the pseudocode made it seem.

Time.now
  |> Task.andThen (\before -> getUserDataViaHttp)
  |> Task.andThen (\_ -> Time.now)
  |> Task.andThen (\after -> 
    -- ???
    -- no access to the before time
    -- in this lambda
  )

This is awkward because we need both before and after at the end of the pipeline, but andThen only has access to the result of the operation immediately previous.

To get around this, we can start nesting the lambdas so the result of each step is always in scope for all the following steps. It’s really ugly but it gets the job done 😬.

timeUserRequest : Task Http.Error Int
timeUserRequest =
  Time.now
    |> Task.andThen (\before ->
      getUserDataViaHttp
        |> Task.andThen (\_ ->
          Time.now
            |> Task.andThen (\after ->
              Task.succeed (after - before)
            )
        )
      )

Haskell

Like Elm, Haskell has a function that can be used to chain async operations (the operator =<<). To get around the nested lambdas we saw in the Elm example, Haskell introduces some syntactic sugar: do notation.

do
  before  <- getCurrentTime
  _       <- getUserDataViaHttp
  after   <- getCurrentTime

  return (after - before)

This desugars into a bunch of chained =<< calls in nested lambdas. The sugared version is much nicer to read though!

Scala

Like Elm and Haskell, Scala also has a function that can chain async operations (theirs is called flatMap). Like Haskell, it has a syntactic sugar for those nested lambdas. It’s called a for comprehension and it’s visually really similar to Haskell’s do notation.

for {
  before  <- getCurrentTime
  _       <- getUserDataViaHttp
  after   <- getCurrentTime
} yield (after - before)

JavaScript

Like all the other languages here, JavaScript has a method for chaining async operations (Promise.prototype.then). It also has some syntactic sugar (async/await) that makes non-linear chains cleaner. If you squint a bit, you’ll see it’s very similar to both Haskell’s do notation and Scala’s for comprehensions.

Note that while getting the current time can be done syncronously in JavaScript, I’m using a function that returns a promise here so the example is similar to the ones we looked at in other languages.

const timeUserRequest = async () => {
  const before = await getCurrentTimeAsync()
  await getUserDataViaHttp()
  const after = await getCurrentTimeAsync()

  return (after - before)
};

Chaining elsewhere

While chaining with then is promise-specific in JavaScript, the other languages examined in this article use their chaining functions for many other constructs - not just for async operations.

For Haskell and Scala, that means that their syntactic sugar notations can be used for all chainable structures. If you work with these languages you will encounter the do / for sugar everywhere from IO operations to null-checking, to JSON parsing.