OK, so we reviewed the core fundamentals of promises, and noticed they're already available in most places.
In practice, most of the time we'll consume and manipulate promises created by another API: a framework, a library, a standard Web API, etc.
So let's start by learning how to best use existing promises: we'll first dive into the nominal scenario, when promises fulfill; then we'll tackle exception management. It's only then that we'll see how to create our own promises when we implement an asynchronous API.
The .then() method and “thenables”
In the previous examples, you certainly noticed a boatload of then() calls. That's to be expected, it's a telltale we're working with promises!
To register a processing for the settlement of a promise, we call its then() method with one or two callbacks:
- The first argument is a fulfillment callback, for success. It is triggered in nominal outcomes.
- The second argument is a rejection callback, for errors. It is usually triggered when exceptions are raised.
Notice that both arguments are technically optional, as you can pass either one. Yet obviously, passing no argument is pretty useless! If your ESLint is properly setup, it will indeed slap your wrist when you try something like that.
You should also note that it's not technically mandatory to be an actual promise in order to interact with a promise chain: all you need is to expose a then() method, which is assumed to have a matching signature. Promises/A+ compliant implements will then be able to insert you into their promise chains. Such not-quite-promise objects are referred to by the rather ugly term “thenable.” For instance, Query objects in the Mongoose library are thenables.
If the callback does not return a promise
So far we're only talking about nominal outcomes, so let's focus on the first callback you'd pass to then(), to handle fulfillment of the promise chain so far.
First, this callback receives the fulfillment value, if any. And you can write any code you want in there. So the question becomes: what can this callback return?
If you don't return explicitly (or use return with no operand), your callback returns undefined. So the chain becomes a promise fulfilled on undefined.
Same thing for any returned value that is not itself a promise: it would get converted to a fulfilled promise, in order to allow the promise chain to continue beyond the current step. To do this, it uses the Promise.resolve() static method: it's a factory that turns its argument into a fulfilled promise on it. We'll soon discuss how it can also be useful to better guard against early exceptions when setting up a promise chain.
The idea behind that behavior is to be able to use synchronous generic transform functions in a promise chain, even if they have nothing to do with promises; for instances, functions that convert, transcode, etc. If they don't need to explicitly return a promise, this makes reusing existing functions a breeze!
Code example: 12-then-non-promise.js
Let's look at this code example:
- We start by fulfilling a promise on 42
- The next step displays that in the console; the log() method doesn't return anything, so we keep going on a promise that is fulfilled on undefined
- The next step logs the current fulfillment, hence undefined, and itself implicitly returns undefined
- Fourth step: we return a string with a single dash, which itself becomes a promise. Its fulfilled value is therefore passed as argument to the first callback of the next step.
- We then convert that to a string of 72 dashes…
- And finally log the result
Note how none of these steps explicitly returned a promise. We're free to return literals, use common methods such as console.log() and the repeat() of Strings, etc. That's handy!
If the callback returns a promise
Let's now assume our callback does indeed explicitly return a promise: what then?
We're touching here on an absolutely critical aspect of promises, that is the foundation of a lot of their power and lets us write code without needless nesting, so I took the time to put together a small animation to try and better drive this point home.
The idea is that the returned promise, be it implicit or explicit, is always the actual “subject” of the next then() call in the chain.
Consider this code snippet: we read a directory, then read the metadata for one of its files, and finally log the size.
Each step needs the previous one: conceptually, we’re looking at a waterfall. With raw callbacks we'd be forced to nest each step in the previous one’s callback. Not so with promises!
- Our initial promise chain has three steps: the readdir(), the first fulfillment callback, then a second fulfillment callback that should operate on the result of the middle step…
- The readdir() happens, and its fulfillment callback gets an array of Strings with the filenames.
- This same callback returns a promise obtained by calling stat().
- This means our returned promise becomes the subject of the next then() call. It “inserts” itself in the chain, so to speak. This is super useful!
- As a return, the next fulfillment callback gets the result value from the stat() promise, that is, the metadata for the file; it destructures it to get the size and displays it.
Let's look at the actual code this snippet came from.
Code example: 13-then-promise.js
As you can see, we had just stripped extraneous details, but the workflow is indeed the same. Let's run this (demo run)
Again, do notice how regardless of the number of steps, and the fact that some of them need the result of the previous one to proceed, we can express our promise chain as a flat list, without needing to do any nesting.
Nesting promises = "I completely missed the point"
This is exactly why, when you see nested promises, you immediately know the author of that code missed a critical aspect of promises; in short, they botched it by simply search-and-replacing the original raw callbacks with promise calls, which sort of misses the entire point.
(back to example 01-horrible-promise-nesting.html)
Seriously, look at this mess. Spaghetti code, my friends. What do you say we clean that up?
(migrating towards 14-clean-promise-nesting)