JavaScript literally cannot do two things at once—it is single-threaded by design. To operate in the browser, where lots of tasks are going on concurrently at all times, it uses events. All you have to do is register an event handler that will execute when something interesting happens.
But the event model, while quick and easy for responding to things like user input, becomes unwieldy when chaining together sets of “do this, wait for that” tasks.
In ES6, we have a standard model for this: the Promise
object.
This is the first is a series on JavaScript Promises:
- How They Work
- How They Break
- How They’ll Work Someday
I, Function, Promise to Return
In modern JavaScript-based applications, both in the browser and on the server, network requests are very common. In the browser, these could be HTTP calls to a server; on the server, these could involve queries to a database. Because JavaScript is single-threaded, it must yield when waiting on a network request. If it did not, no other code could execute, leaving a browser stalled or a server unable to service other requests.
If a function completes execution, but its result will not be available until some event happens (such as a network call returning), it can return a Promise
object–promising to eventually give us what we were really looking for.
(A side note: because ES6 is not necessarily widely available yet in all contexts, I’ll be using much more common ES5 code in this post. You can still use ES6 Promise
s using Jake Archibald’s es6-Promise
polyfill, and in a later post, I’ll show you how ES6 features can make using Promise
s even easier.)
Let’s say we have a function, fetchResult()
, that returns a Promise
.
var resultPromise = fetchResult(query);
While fetchResult()
has begun the process of fetching the result, it doesn’t necessarily have the result when it returns to us. Why is not our concern; it just doesn’t have it yet. So we’ll need to attach some code that will be executed when the result is available, by calling .then()
on the Promise
:
resultPromise.then(function(result) {
console.log('got result', result);
});
This doesn’t do anything right away. But we can now return from our function. After our application is no longer executing JavaScript and when the result becomes available, the console.log
will execute.
Let’s put the two blocks together into a more common pattern, making the process more clear:
fetchResult(query)
.then(function(result) {
console.log('got result', result);
});
This pattern—calling a function that returns a Promise
, then attaching a callback by way of .then()
, is the most common pattern you’ll see in Promise
land. But it’s only the beginning of the story.
Then, and Then, and Then…
So far, this is more concise but not really a whole lot better than attaching event handlers. The real power of Promise
s comes when you chain them:
fetchResult(query)
.then(function(result) {
return processResult(result);
})
.then(function(processedResult) {
console.log('processed result', processedResult);
});
Here’s our friend fetchResult()
back again returning a Promise
that will eventually give us a result, but now we want to further process the result returned. But let’s say that involves a service call, so processResult()
will have to take our result and return us another Promise
.
This chain works because:
- Calling
.then()
on aPromise
(we’ll call this “Promise A”) returns anotherPromise
(“Promise B”). - When Promise A is resolved with a return value, it calls the function inside its own
.then()
. - If Promise A’s
.then()
function returns aPromise
(“Promise C”), Promise A sets up Promise C with its own.then()
to resolve Promise B. - When Promise C resolves with a return value, it calls that
.then()
, which makes Promise B resolve with the return value of Promise C. - With Promise B resolved, its own
.then()
—at line 5 above—is called, and the process starts anew at Step 1.
This continues at every level, whenever we call .then()
on a Promise
. Generally, you don’t have to worry about it—these are implementation details. But the end result, the ability to indefinitely chain Promise
s onto Promise
s, is where their power lies.
Catching Errors
Programmers know that functions can fail. In the network world, functions can fail for many reasons.
In a non-Promise
world, a function can throw an exception. But you can only throw one while your function is still executing. If you don’t know your function failed until later (e.g. a network request timed out or returned bad data), then you need a way to indicate to the caller holding onto your Promise
that you can’t provide the result you promised.
Enter .catch()
:
fetchResult(query)
.then(function(result) {
console.log('got result', result);
})
.catch(function(error) {
console.error(error);
});
The function you provide to .catch()
will be called when fetchResult()
‘s Promise
is rejected, which it would do if it had some issue. In this case, the .then()
is skipped, and only the function in the .catch()
is processed.
It works like this:
fetchResult()
returns Promise A. Promise A’s.then()
returns Promise B. Promise B has a.catch()
attached.- Promise A is rejected with an error. Trying to handle the rejection, it finds it has no
.catch()
attached, and won’t call its own.then()
. So it rejects Promise B. - Promise B, being rejected, calls its
.catch()
with the error from the rejection.
With this pattern in play, you can use one .catch()
to handle a string of .then()
s:
fetchResult(query)
.then(function(result) {
return processResult(result);
})
.then(function(processedResult) {
console.log('processed result', processedResult);
});
.catch(function(error) {
console.error(error);
});
Making Our Own JavaScript Promises
Now that you know how to handle a Promise
, how about returning them?
If your Promise
-returning function is internally dealing with Promise
s from other functions, it’s really easy to do. Just return the chain:
function fetchAndProcessResult(query) {
return fetchResult(query)
.then(function(result) {
return processResult(result);
});
}
Given that fetchResult()
is returning Promise A, and its .then()
is returning Promise B, our function returns Promise B. Promise B will resolve when processResult()
resolves its own Promise
, so callers can operate like this:
fetchAndProcessResult(query)
.then(function(processedResult) {
console.log('processed result', processedResult);
})
.catch(function(error) {
console.error(error);
})
If you’re working with an API that doesn’t natively support Promise
s, though, you’ll have to build a Promise
yourself and wire it up appropriately. Here’s an example for fetchResult
, which is using a database connection that returns results via events—one for results and one for errors:
function fetchResult(query) {
return new Promise(function(resolve, reject) {
db = new DatabaseConnection();
// These can also be written:
// db.on('results', resolve);
// db.on('error', reject);
db.on('results', function(results) {
resolve(results);
});
db.on('error', function(error) {
reject(error);
});
db.query(query);
});
}
The Promise
constructor takes a function (an executor) that will be executed immediately and passes in two functions: resolve
, which must be called when the Promise
is resolved (passing a result), and reject
, when it is rejected (passing an error). Additionally, if the executor happens to throw an exception, the constructor will automatically convert it to a rejection.
Make sure you cover all possible resolutions and rejections—if you don’t, your function may leave its caller hanging indefinitely.
What’s Next
Now you have a solid grounding in how ES6 Promise
s work. But even though they represent the best available solution for dealing with asynchronous programming in JavaScript, there are plenty of pitfalls to avoid. In my next post, I’ll take you through a number of common mistakes, many of which I’ve made myself, that break Promise
s.
This is the first is a series on JavaScript Promises:
- How They Work
- How They Break
- How They’ll Work Someday