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.
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.
Let’s consider a simple futures example (using fluture-js) where we:
- Read and parses a package.json file to get the name of a package
- Send that data to a fictional API that returns some metadata about it
- Parse that result to get the downloads count
- Sends that count to an
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
- encaseP creates a future from a promise
- node creates a future from a node-style async function
2. pipe lets you chain futures
Think of this like the
pipe() that you get when you invert the
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
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.
- 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.
- 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.
- Better control flow than promises for sure. You can
parallel()-ize, and cancel one or more futures out of the box.
- 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.
Real life example
As a more concrete example, I’ve written a CLI application for parsing and searching JSON data with futures. The application itself is stateless and can parse and search JSON files much bigger than the system’s memory limit.
Originally published at https://write.as on June 5, 2019.