Promises in JavaScript
In JavaScript, most of the code that we write falls under asynchronous programming. Whenever we pass an argument as a function and expect this function to run sometime in future, we’re dealing with something asynchronous. And it isn’t always nice to have callbacks as the preliminary entities for async. We frequently end up with this
a('a', function() {
b('b', function() {
c('c', function() {
d('d', function() {
console.log('WTF!!!');
});
});
});
});
and it was never nice to add one more function to represent your idea into code via callbacks. Promises are a big step towards solving this issue. The following tutorial on Promises by Jake Archibald(@jaffathecake) is one of the best out there. I’d recommend reading it if you’re new to Promises.
The analogy
In the synchronous world, the primitives are values.
var foo= 5;
var bar = {};
var baz = 'dice';
So, in the asynchronous world, we need something that can be used as a neat representation of async values. When an asynchronous function is invoked, instead of returning the value immediately, it returns a placeholder for the value — a Promise and this placeholder provides a way to ask for the actual value.
var promise = asyncfn();
That’s it and we can pass this value around. A better overview of this entire space can be found in the General Theory of Reactivity (gtor) by Kris Kowal.
A Stateful Machine
Promises are stateful objects themselves and can exist in one of these three states: unfulfilled, resolved, rejected. It’s state properties —
- When a promise is created, it is always in the unfulfilled state
- Once a promise changes its state from unfulfilled to either resolved or rejected, it cannot change again
- If you’re adding to the tip of a Promise chain, you’re creating a new Promise and the state properties apply
First class citizen
A promise can be treated as a first-class citizen resolving to a JavaScript value in future. Once a promise has been created, the value it resolves to has the same properties as a normal JavaScript value. To understand this better, let’s take an example,
var x = 1;
var y = x + 5;
and,
var x = delay1s(1);
var y = x.then(function(val) { return val + 5; });
As you can see the analogy, in both the programs, x can be used even after y has consumed x. And the original value of x, sync or async, doesn’t change.
Single output pattern
This is a very common pattern (in structured programming) where you start with an async value (a Promise) and at every stage you’re storing only the tip of the chain. So all branches created eventually merge using all / race. Storing mid-references are not allowed or the notion of stored mid references becomes irrelevant. During branching you only store until all branches are merged back. The ES7 async/await enforces this single output pattern through its syntax.
Concurrent usage pattern
This is also a legitimate use case for Promise. With this, you can create custom concurrent executional paths and may expose any intermediate promise. Usually, this is hidden using some higher level abstractions and what gets exposed is determined by the abstraction rules. It uses branching and merging underneath.
The Mutate anti-pattern
Take this example — A promise that resolves to an Object after 100ms.
var promise = delay(100).then(_ => ({ a: 5 }))
Objects are mutable in JavaScript and are passed by references. When you branch this promise out,
var x = promise.then( v => { v.a = 6; return v })
var y = promise.then( v => { console.log(v); return v })
The anti-pattern: the tip x has a consumer that modifies v
Side-effect: the tip y gets a value where v.a is 6, where you’d expect v.a to be 5.
I’d recommend further reading about Promise anti-patterns in bluebird’s documentation.
Epilogue
Promises are the asynchronous primitives in JavaScript analogous to values which are synchronous primitives. There is a lot more to talk about promise based async programming patterns — I’d hopefully try that in my future posts after a few clarifications and learnings.