JavaScript Promises – How They Break

In my previous post, I took you through an introduction and gave a peek under the hood for ES6 Promises, showing you how they work and how to use them. Today, I’m going to talk about how JavaScript Promises can break. Hopefully, this will equip you to track down Promise bugs in code that fails in mysterious ways.

This is the 2nd is a series on JavaScript Promises:

Breaking the Chain

Here’s the chaining example from our last post, with a slight change that breaks it entirely. See if you can spot it:

fetchResult(query)
    .then(function(result) {
        processResult(result);
    })
    .then(function(processedResult) {
        console.log('processed result', processedResult);
    });
    .catch(function(error) {
        console.error(error);
    });

What’s wrong with the above example? Line 3 is missing the return. The way this changes the chain is subtle:

  1. When the Promise returned by fetchResult() resolves, its .then() is called.
  2. processResult() is called, which returns a Promise. Its returned Promise is never attached to anything, so it begins executing–but nobody will ever be notified of its result. This is especially insidious if processResult() is doing something like kicking off a database update—the update will almost certainly happen, even though your code won’t know about it.
  3. The function containing processResult() will return nothing—undefined–and automatically wrap it in a resolved Promise.
  4. The next .then() will execute immediately, receive undefined as processedResult, and output it.

As far as broken Promise chains go, this one just throws away a result. But as any JavaScript programmer knows, undefined floating around where a value is expected can cause all sorts of havoc.

To avoid this problem, you can adopt the pattern where every step in your chain returns a Promise, either by creating one on the spot (hint: try Promise.resolve() if you already have the value you want to return, or simply want to resolve that step without returning anything at all) or by returning the result of a Promise-generating function. Then you’ll never have to guess whether a particular .then() was supposed to return a value or not.

Personally, I think that letting .then() implementations return non-Promise values was a design mistake. Explicit intent is better than implicit behavior, and without the magic wrapping of bare values as an option, Promise implementations could have thrown exceptions when a .then() returned a non-Promise. This would clearly show where errors were made and reduce the risk of weird behavior, at the expense of a little verbosity.

Jumping the Gun

You don’t always need to put a full inlined function declaration in a .then(). In fact, it can dramatically improve readability if you factor out steps into named functions. Here’s that chain we’ve been working on again, using this strategy:

fetchResult(query)
    .then(processResult)
    .then(function(processedResult) {
        console.log('processed result', processedResult);
    });
    .catch(console.error);

But be careful that you’re giving .then() a function reference, and not a call. This

fetchResult(query)
    .then(processResult())
    .then(function(processedResult) {
        console.log('processed result', processedResult);
    });
    .catch(console.error);

will behave very oddly. fetchResult() will start first. But processResult() will not only be called with no arguments; it will be called immediately, without even waiting for fetchResult() to run.

This happens because we’re not giving .then() a function that it can call on its own terms–we’re giving it the results of a function call. Regrettably, .then() doesn’t flag this as any sort of error—it just passes undefined to the next link in the chain.

The fact that Promise-generating functions start their work immediately can confuse developers a bit in other ways, too:

fetchResult(query)
    .then(processResult);
    .then(console.log);

markProcessingDone(query)
    .then(console.log);

Here, markProcessingDone() does not wait for the fetchResult(query) chain to complete. It will start immediately, running effectively in parallel with the first chain. This is a problem particularly if anything in the first chain fails—we’d be marking our processing as done, when in fact it failed.

Problems up Front

One really nice behavior of Promise chains is that an unexpected exception thrown in .then()s will be converted automatically to a rejected Promise, which then filters through to a .catch().

Unfortunately, because Promises are not a language feature but an API, they can’t do a thing about the first link in the chain. Refer back to our all-in-one fetch and process function (which we’ve now simplified a bit as per the last section):

function fetchAndProcessResult(query) {
    return fetchResult(query)
        .then(processResult);
}

If fetchResult() throws an exception, that exception is going to bubble up to the caller without returning a Promise. Maybe that’s okay for your caller. Unfortunately, I’ve seen several examples where it’s not. If that’s the case, you can use this one weird trick:

function fetchAndProcessResult(query) {
    return Promise.resolve(query)
        .then(fetchResult)
        .then(processResult);
}

This adds a little overhead in the form of an extra immediately-resolved Promise, but it has the advantage that any exceptions raised in the call to fetchResult will be caught and converted to a rejected Promise, ready for our caller’s .catch().

JavaScript Promises Live Forever

One thing you’ll need to be very careful about when constructing your own Promises is to make absolutely sure you’ve covered all the possibilities and routed them to either resolve or reject. You’ll also need to watch out for unexpected paths your code can take, so keep it simple.

If you’re not confident that your new Promise will ever be resolved or rejected, you might want to consider adding a timeout. Here’s an addition to the fetchResult() function from the last installment, now with a five-second timeout:

function fetchResult(query) {
    return new Promise(function(resolve, reject) {
        …

        setTimeout(function() {
            reject(new Error('timed out'));
        }, 5000);

        …
    });
}

This relatively straightforward pattern helps cover you when the code or the service you’re using might leave you hanging. It allows you to make sure your caller isn’t left hanging, too.

Go Forth and Promise

Some of these issues may seem a bit contrived. But despite Promises being our best option for asynchronous programming in JavaScript right now, their paradigm still causes mental friction. In turn, this can make even experienced developers make mistakes like these.

But there’s hope. In my next post, I’ll show you how some ES6 features can make Promises easier to use. I’ll also show you features from future versions of JavaScript that will make asynchronous programming as painless as possible, while still remaining compatible with code that uses Promises.


This is the 2nd is a series on JavaScript Promises: