Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 982
Stephan G Avatar asked Jul 28 '16 00:07

Stephan G


People also ask

How do you catch errors in promises?

catch " around the executor automatically catches the error and turns it into rejected promise. This happens not only in the executor function, but in its handlers as well. If we throw inside a . then handler, that means a rejected promise, so the control jumps to the nearest error handler.

What happens if Promise is not resolved?

A promise is just an object with properties in Javascript. There's no magic to it. So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.

How do you handle rejection promises?

We must always add a catch() , otherwise promises will silently fail. In this case, if thePromise is rejected, the execution jumps directly to the catch() method. You can add the catch() method in the middle of two then() methods, but you will not be able to break the chain when something bad happens.

How do I return a Promise error?

reject or throw when you want to return a rejected promise (a promise that will jump to the next . catch() ). @maxwell - I like you example. In the same time if on the fetch you will add a catch and in it you throw the exception then you will be safe to use try ...


1 Answers

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.

like image 146
jfriend00 Avatar answered Oct 03 '22 06:10

jfriend00