DEV Community

Nadeesha Cabral
Nadeesha Cabral

Posted on • Originally published at write.as

From Promises to Futures in Javascript

As someone who jumped on the Node.js train early on and wrestled with the callback hell, I quite liked promises. I still do, but more in an "it's the best that we've got way". So many times I've forgotten to catch a promise chain and it always decided to silently fail. Or, I'd have to hack my way through trying to cancel a promise chain.

I won't go into too much detail over all of the issues with promises. But I highly recommend Broken Promises which does an excellent job of summarizing everything.

Fluture

As an alternative to promises, I've been using fluture for some time now. I highly recommend the fluture-js library for this. It introduces itself as a "Fantasy Land compliant (monadic) alternative to Promises". If you're unfamiliar with monads of fantasy-land specification, don't worry about it.

Douglas Crockford once said, "Once someone understands Monads, they lose the ability to explain it to anybody else". Despite that, let me give a shot at explaining what a monad is, in Javascript terms.

In Javascript world, a monad tends to be an object that's got a bunch of functions as properties. When you invoke these functions, it tends to do something and return something like the original Javascript object. You can then use that returning object to do further computations. This might sound like a promise, but promises are not monads for reasons which we'll not get into here.

Example

Let's consider a simple futures example (using fluture-js) where we:

  1. Read and parses a package.json file to get the name of a package
  2. Send that data to a fictional API that returns some metadata about it
  3. Parse that result to get the downloads count
  4. Sends that count to an onSuccess function
import { encaseP, node, encase } from "fluture";

const readFileF = filePath => node(done => fs.readFile(filePath, "utf8", done));

const fetchF = url => encaseP(url => fetch(url));

const jsonParseF = encase(jsonString => JSON.parse(jsonString));

const getPackageDownloads = (npmPackageName, onSuccess, onFailure) => {
  readFileF("package.json")
    .pipe(chain(jsonParseF)) // shorthand for chain(jsonString => jsonParseF(jsonString))
    .pipe(map(package => package.name))
    .pipe(
      chain(packageName => fetchF(`/api/v1/package-metadata/${packageName}`))
    )
    .pipe(map(jsonParseF))
    .pipe(map(response => response.data.metadata.downloadCount))
    .pipe(
      fork(error => onFailure(error), downloadCount => onSuccess(downloadCount))
    );
};

Explanation

I'd like to believe that you can elicit 80% of the value that fluture gives by knowing 20% of the constructs it provides. And this might well be that 20%. Let's go through each construct of the earlier example in detail and see what it does.

0. Common to all constructs...

...is the fact that everything returns a future. Therefore, we can compose these futures, refactor them out, etc.

1. encaseP, node, encase creates futures

Since javascript doesn't have futures, we need to have some tools to convert existing Javascript constructs like promises, and functions to futures. And these 3 constructs does exactly that.

  1. encaseP creates a future from a promise
  2. node creates a future from a node-style async function
  3. encase creates a future from a plain old javascript function

Since all three of these Javascript constructs may fail through a promise rejection, a callback(error) or an exception, these three utilities map that rejection state to future rejection as well.

2. pipe lets you chain futures

Think of this like the pipe() that you get when you invert the compose() function. We talked about this in length at A practical guide to writing more functional JavaScript. It basically lets you "chain" futures.

3. map transforms values

When you have to pipe something, you always have to pipe a future-ed version of your result. When your computation doesn't produce a future, like package => package.name, you can do that transformation inside of map.

When you invoke map(fn), with a future, map takes the value inside that future, applies fn to transform that value, and returns the value wrapped in a future.

4. chain transforms values but expects a future back

chain does the same thing map doesn't, but the fn in your chain(fn) must return a future back.

5. fork to execute

Since futures are lazily evaluated, nothing is being done until you tell the future to execute things. fork(failureFn, successFn) takes a success function and a failure function and executes them on a success/failure instance.

Why use futures over promises?

There's a lot of advantages to using futures. Aesthetically pleasing API is a big part of it. Since promises are the real competition, let me try to make a few concrete distinctions against promises.

  1. Lazy evaluation has a lot of practical advantages. You have a guarantee that your computation will not execute at the time of creating the future. Whereas the new Promise(...) will execute at the time of the creation.

  2. Testability comes through lazy evaluation as well. Instead of mocking all of your side-effects when testing, you can assert whether the futures wrapping the side effects were "composed". Thereby not executing the side-effects as well.

  3. Better control flow than promises for sure. You can race(), parallel()-ize, and cancel one or more promises out of the box.

  4. Error handling is far more superior. You will always end up with an error. No more forgotten catch()es silently suppressing errors. And you get a really good debugging experience out of the box.

Latest comments (0)