Stephan G
Stephan G

Reputation: 3473

ES6 Promise Errors not bubbling up as expected

I am starting with E6 Promises. I like them very much, but there is a crucial concept around error handling that I don't understand and would love some clarification on.

Let's assume the following simple function that returns a promise:

    function promiseString(str, timeout, doResolve) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if (doResolve) {
                    resolve(str);
                } else {
                    reject(new Error("Rejecting " + str));
                }
            }, timeout);
        });
    }

It is pretty straightforward, just returns a promise for the string that was passed to it, and causes that promise to be resolved or rejected (based on the third argument) in "timeout" milliseconds.

I can consume this completely as expected as follows:

            promiseString("One", 100, true)
                .then((str) => { console.log("First then is " + str); return promiseString(str + " two", 100, true); })
                .then((str) => { console.log("Second then is " + str); return promiseString(str + " three", 100, true); })
                .then((str) => console.log(str))
                .catch((err) => console.error(err));

If alter the third argument to from "true" to "false" in any of the calls in this chain, my error is caught as expected and send to console.error().

However, now imagine the following (similarly silly) function for constructing a promising object:

    function DoublePromiser(str1, str2, doResolve) {
        this.promise = new Promise((resolve, reject) => {
            promiseString(str1, 100, doResolve)
                .then((s1) => promiseString(s1 + str2, 100, doResolve))
                .then((s2) => resolve(s2));
        });
    }

Imagine now that I consume this code as follows, with everything resolving and nothing rejecting, (doResolve is set to true):

            var dp = new DoublePromiser("Big", "Promise", true);
            dp.promise
                .then((s) => console.log("DoublePromise: " + s))
                .catch((err)=>console.log("I did catch: ", err.message));

As would be expected, I see the following in the console:

DoublePromise: BigPromise

However, now I alter the consuming code, setting doResolve to "false" (which causes my promise routine to reject):

            var dp = new DoublePromiser("Big", "Promise", false);
            dp.promise
                .then((s) => console.log("DoublePromise: " + s))
                .catch((err)=>console.log("I did catch: ", err.message));

Because of my understanding of how errors should "bubble up", I would expect the console to log as follows:

I did catch: Rejecting Big

But it does not. Instead, the console shows an uncaught error:

Uncaught (in promise) Error: Rejecting Big

I only get what I expect (and desire) if I add a catch to the end of the chain in the DoublePromiser, like this:

    function DoublePromiser(str1, str2, doResolve) {
        this.promise = new Promise((resolve, reject) => {
            promiseString(str1, 100, doResolve)
                .then((s1) => promiseString(s1 + str2, 100, doResolve))
                .then((s2) => resolve(s2))
                .catch((err) => reject(err)); // ADDING THIS TO MAKE IT WORK
        });
    }

Now I get what I expect, the error is not uncaught. But this seems counter to the whole idea that errors bubble up, and it seems weird to catch an error just to re-reject the same error.

Am I missing a way of getting this to work simply?

Am I missing some fundamental concept?

Upvotes: 5

Views: 4827

Answers (1)

jfriend00
jfriend00

Reputation: 707686

You are using a promise constructor anti-pattern. Don't wrap an existing promise in another promise you make yourself as that just makes you do lots of extra work to make things work properly and since most people don't do that extra work properly, it's also very prone to programming mistakes. Just return the promise you already have.

Change this:

function DoublePromiser(str1, str2, doResolve) {
    this.promise = new Promise((resolve, reject) => {
        promiseString(str1, 100, doResolve)
            .then((s1) => promiseString(s1 + str2, 100, doResolve))
            .then((s2) => resolve(s2))
            .catch((err) => reject(err)); // ADDING THIS TO MAKE IT WORK
    });
}

to this:

function DoublePromiser(str1, str2, doResolve) {
    return promiseString(str1, 100, doResolve)
       .then((s1) => promiseString(s1 + str2, 100, doResolve));
}

And, then just use it as a function:

DoublePromiser("Big", "Promise", false).then(...);

Recap: You nearly always want to return inner promises from within .then() handlers because this allows nested errors to propagate upwards and also properly chains/sequences async operations.

And, you want to avoid wrapping new promises around existing promises nearly always because you can just chain to and/or return the existing promise you already have.

Also, be aware that if you do a .catch(), that will "handle" a rejected promise and return a new non-rejected promise, continuing the promise chain from there unless inside the .catch() handler you return a rejected promise or throw an exception. So, this:

p.catch((err) => console.log(err)).then(() => console.log("chain continues"))

will happily do both console.log() statements because the .catch() "handled" the promise so the promise chain happily continues.


As I said in my earlier comments, these 100% theoretical discussions are hard to get to exactly what you really want to accomplish (we have to guess what the real problem is) without a 20 page tutorial on how promises work that covers a lot of stuff. It's a lot better if you post a real-world problem you're trying to solve with this technique and we can show/explain the best way to do that in a few lines of code and a few paragraphs of explanation.

Upvotes: 10

Related Questions