Promises
We know how to access return values (in the callback sense) in continuation-passing style:
function roll(callback) {
setTimeout(() => {
callback(Math.floor(Math.random() * 5));
}, 500);
}
roll((number) => {
console.log(`You just rolled a ${number}`);
});
Concurrent programming predates JavaScript by many years: getting the response of various database queries or network requests, reading multiple files. Nesting callbacks became tedious and very cumbersome when iterating over a list, so concepts like thunks, futures and deferred objects helped to better organize those operations.
In the ES2015 revision the Promise object was introduced in JavaScript.
const result = new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); console.log('before'); result.then((number) => { console.log(`You just rolled a ${number}`); }); console.log('after');
Toggle Console Output
First, the log sandwich works like all the examples of this chapter - before, after and after the interval the roll.
Let's break things down:
- in line 1 a new
Promiseobject is instantiated and assigned to therollvariable; - the
Promiseconstructor accepts one function as parameter; - this function accepts a
resolveparameter on its own; resolveis a function, that is called in line 3 (inside thesetTimeoutcallback).
That's already a lot to digest. Let's finish before reviewing:
- the
rollinstance has a.then()method, called in line 8; .then()accepts a function as a parameter, in this case an arrow function;- the arrow function gets what was passed to
resolveas argument.
We define a Promise as follows:
A Promise is a proxy for a value not necessarily known when the promise is created.
When the value will be available, it will be accessible inside the .then() callback. Until now the code is not that much different from our roll() function. We'll see other features that Promises offer in a moment.
Before moving on, let's use the Promise object inside a function, to make it reusable:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } roll().then((number) => { console.log(`You just rolled a ${number}`); }); roll().then((number) => { console.log(`You just rolled a ${number}`); });
Toggle Console Output
See how:
- the
rollfunction now accepts no parameters (no callback); - the
rollfunction has areturnstatement; rollis called twice, each time followed by a.then();- each
.then()receives the result of each roll.
One can also write a single logResult function and pass it to both .then()s:
function roll() { return new Promise((resolve) => { setTimeout(() => { resolve(Math.floor(Math.random() * 5)); }, 500); }); } function logRoll(number) { console.log(`You just rolled a ${number}`); } roll().then(logRoll); roll().then(logRoll);
Toggle Console Output
The .then() name wasn't chosen on a whim, it makes you read the code as a sentence!
By now you should be on the verge of breaking. That's a lot of bracket and arrow soup, but you are doing some great syntax workout. Callbacks are the core feature of JavaScript, and even though we'll reach soon the syntax that hides a lot of them, you won't go far without knowing what's going on under the hood.
Throwing and handling errors in continuation-passing style
Let's leave the dice apart for a moment and let's write an async division function in continuation-passing style. First let's review the sync version:
function divide(a, b) { return a / b; } console.log(divide(1, 2));
Toggle Console Output
- 0.5
We want to be sure the second parameter is different from 0 in order to keep the image of the function in the real numbers. Let's throw an error in such case:
function divide(a, b) { if (b === 0) { throw new Error('Cannot divide by zero!'); } return a / b; } try { console.log(divide(1, 2)); console.log(divide(1, 0)); } catch(error) { console.log(error); }
Toggle Console Output
- 0.5
- Cannot divide by zero!
We haven't discussed how to throw errors in CPS. We cannot use a throw statement, because it wouldn't be caught at the right time by a try...catch block. An agreed method is to pass the possible error as an actual parameter to the callback, like in the Node.js standard library:
fs.readFile('/path/to/file.txt', (error, contents) => {
if (error) {
console.log('Error opening file', error);
return;
console.log('File contents', contents);
}
});
Important: the error and the further callback parameters cannot both hold any value at once, it's either / or! In functional programming this structure is actually called Either.
Let's do the same in our CPS implementation:
function divide(a, b, callback) { if (b === 0) { callback(new Error('Cannot divide by zero!')); return; } setTimeout(() => { callback(null, a / b); }, 500); } function handleResult(error, result) { if (error) { console.log(error); return; } console.log(result); } divide(1, 2, handleResult); divide(1, 0, handleResult);
Toggle Console Output
Let's review what we have just done:
- the
dividesignature accepts a third parameter, thecallbackfunction; - the callback accepts two parameters - a possible
error, and theresultis there is no error to be thrown; - we handle the error before the timeout, since it can be checked right away;
- in the timeout, we call
callback(null, a / b).
It's important to pass null as the first argument. In this way we can decide if we are in the successful case in the callback body.
Throwing and handling errors in promises
Let's rewrite the divide function with a Promise, without error handling first:
function divide(a, b) { return new Promise((resolve) => { setTimeout(() => { resolve(a / b); }, 500); }); } function handleResult(result) { console.log(result); } divide(1, 2).then(handleResult); divide(1, 0).then(handleResult);
Toggle Console Output
See what happens if we don't handle the error? Infinity! We don't want to deal with Infinity. As in the previous translation:
- no callback as third parameter to
divide; - a
Promiseis returned from the function; resolvegets called (without any error reference).
What about the error now? The function we pass to the Promise constructor accepts a second parameter, namely called reject:
function divide(a, b) { return new Promise((resolve, reject) => { if (b === 0) { reject(new Error('Cannot divide by zero!')); return; } setTimeout(() => { resolve(a / b); }, 500); }); } function handleResult(result) { console.log(`Handling result: ${result}`); } function handleError(error) { console.log(`Handling error: ${error}`); } divide(1, 2) .then(handleResult) .catch(handleError); divide(1, 0) .then(handleResult) .catch(handleError);
Toggle Console Output
- We call
rejectwith ourErrorinside the if statement in lines 3/5; - At the end of the program we call
dividetwice; - In both cases we follow the
.then()with a.catch(); - In the 1/2 case, just
handleResultis called; - In the 1/0 case, just
handleErroris called.
Since the Promise callback constructor accepts two parameters (resolve and reject), it exposes two different handlers for the success and failure case: .then() and .catch(). But why can we call .catch() after .then()?
The verbose version would actually be:
function divide(a, b) { return new Promise((resolve, reject) => { if (b === 0) { reject(new Error('Cannot divide by zero!')); return; } setTimeout(() => { resolve(a / b); }, 500); }); } function handleResult(result) { console.log(`Handling result: ${result}`); } function handleError(error) { console.log(`Handling error: ${error}`); } const firstResult = divide(1, 2); firstResult.then(handleResult) firstResult.catch(handleError); const secondResult = divide(1, 0); secondResult.then(handleResult) secondResult.catch(handleError);
Toggle Console Output
That's a lot of repetition! The trick is that .then() actually returns a Promise again, so you can call .catch() on that.
The Promise object exposes more methods than just the constructor, then and catch. It's not relevant to expose then now. It's way more important to focus on the transition from CPS to Promise-based functions, and for that the aforementioned methods are enough.
We can now expand the definition of Promise with the following fact:
A Promise is in one of these states:
- pending: initial state, neither fulfilled nor rejected.
- fulfilled: meaning that the operation was completed successfully.
- rejected: meaning that the operation failed.
Comparing the two versions
Let's have a look:
// Continuation-passing style
function divide(a, b, callback) {
if (b === 0) {
callback(new Error('Cannot divide by zero!'));
return;
}
setTimeout(() => {
callback(null, a / b);
}, 500);
}
function handleResult(error, result) {
if (error) {
console.log(error);
return;
}
console.log(result);
}
divide(1, 2, handleResult);
divide(1, 0, handleResult);
// Promise style
function divide(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(new Error('Cannot divide by zero!'));
return;
}
setTimeout(() => {
resolve(a / b);
}, 500);
});
}
function handleResult(result) {
console.log(`Handling result: ${result}`);
}
function handleError(error) {
console.log(`Handling error: ${error}`);
}
divide(1, 2)
.then(handleResult)
.catch(handleError);
divide(1, 0)
.then(handleResult)
.catch(handleError);
We got back to a syntax that is similar to try...catch. No possible null errors, no explicit callbacks passed to the original divide function.
Conclusion
Are Promise-based function better than CPS then? I don't find it a well posed question, since they serve different purposes. It's undeniable that Promises reduce syntax complexity when calling the original function since no explicit callback is needed; we have to provide some continuation in the .then() anyhow, so you'll appreciate a lot the introduction to async/await in a couple of lessons from now.