2

I'm having trouble to properly catch an error/reject in a promise chain.

const p1 = () => {
    return new Promise((resolve, reject) => {
        console.log("P1");
        resolve();
    });
};

const p2 = () => {
    return new Promise((resolve, reject) => {
        console.log("P2");
        reject();
    });
};


const p3 = () => {
    return new Promise((resolve, reject) => {
        console.log("P3");
        resolve();
    });
};

p1().catch(() => {
    console.log("Caught p1");
}).then(p2).catch(() => {
    console.log("Caught p2");
}).then(p3).catch(() => {
    console.log("Caught p3");
}).then(() => {
    console.log("Final then");
});

When the promise is rejected, the following .then still gets executed. In my understanding, when in a promise chain an error/reject happened, the .then calls that follow it are not executed any more.

P1
P2
Caught p2
P3
Final then

The rejection gets caught correctly, but why is "P3" logged after the catch?

What am I doing wrong?

To clarify @evolutionxbox, this is my expected result:

Promise.resolve().then(() => {
    console.log("resolve #1");
    return Promise.reject();
}).then(() => {
    console.log("resolve #2");
    return Promise.resolve();
}).then(() => {
    console.log("resolve #3");
    return Promise.resolve();
}).then(() => {
    console.log("Final end");
}).catch(() => {
    console.log("Caught");
});

This code works exactly like it should. And I can't see a difference to my code, except that I declared the functions separately.

The code above stops no matter where the promise is rejected.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Marc
  • 2,920
  • 3
  • 14
  • 30
  • The first `.catch()` after the rejection takes care of the error and after that the default chain is continued. – Sirko Mar 01 '21 at 20:05
  • @Sirko Thats exactly what happens. But *why*? I thought that the chain is "aborted" on the first reject. How can i stop the execution of the following function on a reject? And why cant i just use a "global" catch for all promises? – Marc Mar 01 '21 at 20:07
  • You're calling p3 after you catch the error from p2. The catch handles the error, then proceeds as if it didn't happen – Charlie Bamford Mar 01 '21 at 20:08
  • 1
    The return value of `catch` is a promise so it will have a then method right? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch why do you think it should stop? – evolutionxbox Mar 01 '21 at 20:10
  • @evolutionxbox Please see my updated question. Because that excatly how chaining should work. See the attached (correct) working example. – Marc Mar 01 '21 at 20:17
  • 1
    You are just not correct in your assumptions, that's all. .catch doesn't stop promise chain for the same reason regular try .. catch doesn't stop code execution of a code located after catch. It wouldn't make sense if it did – Max Mar 01 '21 at 20:20
  • @Max Ok, makes sense. But why if i remove any other catch block except the last one, i get a "promise rejection" warning in node? – Marc Mar 01 '21 at 20:22

4 Answers4

3

Here is a synchronous equivalent of your code:

const f1 = () => {
  console.log("F1");
};

const f2 = () => {
  console.log("F2");
  throw new Error();
};

const f3 = () => {
  console.log("F3");
};

try {
  f1();
} catch {
  console.log("Caught f1");
}

try {
  f2();
} catch {
  console.log("Caught f2");
}

try {
  f3();
} catch {
  console.log("Caught f3");
}

console.log("Final code");

As you can see, that gives a matching result. Hopefully, looking at the synchronous code you would not be surprised why. In a try..catch you are allowed to attempt recovery. The idea is that the catch will stop the error propagation and you can hopefully continue further. Or if you do want to stop, you still have to explicitly throw again, for example:

doCode();

try {
    makeCoffee();
} catch(err) {
    if (err instanceof IAmATeapotError) {
        //attempt recovery
        makeTea();
    } else {
        //unrecoverable - log and re-throw
        console.error("Fatal coffee related issue encountered", err);
        throw err;
    }
}

doCode();

This is also the purpose Promise#catch() serves - so you can attempt recovery or at least act when there was a problem. The idea is that after the .catch() you might be able to continue:

const orderPizza = (topping) => 
  new Promise((resolve, reject) => {
    if (topping === "pepperoni")
      reject(new Error("No pepperoni available"));
    else
      resolve(`${topping} pizza`);
  });

const makeToast = () => "toast";
const eat = food => console.log(`eating some ${food}`);

async function main() {
  await orderPizza("cheese")
    .catch(makeToast)
    .then(eat);
    
  console.log("-----");
  
  await orderPizza("pepperoni")
    .catch(makeToast)
    .then(eat);
}

main();

In order to reject the promise chain from a .catch() you need to do something similar as a normal catch and fail at the error recovery by inducing another error. You can throw or return a rejected promise to that effect.

This code works exactly like it should. And I can't see a difference to my code, except that I declared the functions separately.

The code above stops no matter where the promise is rejected.

The second piece of code you show fails entirely after a reject because there are no other .catch()-es that are successful. It is basically similar to this synchronous code:

try {
    console.log("log #1");
    throw new Error();
    console.log("log #2");
    console.log("log #3");
    console.log("Final end");
} catch {
    console.log("Caught");
}

Thus if you do not want to recover early, you can also skip the .catch() instead of inducing another error.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
1

Try this.

const p1 = (arg) => {
  // Promise returns data in the respected arguments
  return new Promise((resolve, reject) => {
    // Data to be accessed through first argument.
    resolve(arg);

  });
};


const p2 = (arg) => {
  return new Promise((resolve, reject) => {

    // Data to be accessed through second argument.
    reject(arg);

  });
}

p1('p1').then(resolve => {
  console.log(resolve + ' is handled with the resolve argument. So it is accessed with .then()');
}) // Since reject isn't configured to pass any data we don't use .catch()

p2('p2').catch(reject => {
  console.log(reject + ' is handled with the reject argument. So it is accessed with .catch()');
}) // Since resolve ins't configured to pass any data we don't use .then()

// You would normally configure a Promise to return a value on with resolve, and access it with .then() when it completes a task successfully.

// .catch() would then be chained on to the end of .then() to handle errors when a task cannot be completed.

// Here is an example.

const p3 = () => {
  return new Promise((resolve, reject) => {

    var condition = true;

    if (condition === true) {
      resolve('P3');
    } else {
      reject('Promise failed!');
    }

  });
};

p3('p3').then(resolve => {
  console.log(resolve);
}).catch(reject => {
  console.log(reject);
})
0

You do not do anything wrong. In your code you call the first promise p1. Then you write p1.catch(...).then(...).then(...).then(...). This is a chain which means that you should call then 3 times, because you called resolve method in the p1 promise (all these thens depend on the first promise).

Valery
  • 295
  • 2
  • 12
  • That was not my question. Why is the chain not "aborted" if i reject a promisie. The rest is still executed. I updated my question, with a working example of how my code should work. – Marc Mar 01 '21 at 20:16
0

When the promise is rejected, the following .then still gets executed.

Yes. Just to be accurate: the then and catch method calls are all executed synchronously (in one go), and so all promises involved are created in one go. It's the callbacks passed to these methods that execute asynchronously, as the relevant promises resolve (fullfill or reject).

In my understanding, when in a promise chain an error/reject happened, the .then calls that follow it are not executed any more.

This is not the case. The promise that a catch returns can either fullfill or reject depending on what happens in the callback passed to it, and so the callbacks further down the chain will execute accordingly when that promise resolves.

The rejection gets caught correctly, but why is "P3" logged after the catch?

As in your case the catch callback returns undefined (it only performs a console.log), its promise fullfulls! By consequence, the chained then callback -- on that promise -- is executed... etc.

If you want to "stop"

If you want to keep the chain as it is, but wish to have a behaviour where a rejection leads to no further execution of then or catch callbacks, then don't resolve the associated promise:

const stop = new Promise(resolve => null);

const p1 = () => {
    return new Promise((resolve, reject) => {
        console.log("P1");
        resolve();
    });
};

const p2 = () => {
    return new Promise((resolve, reject) => {
        console.log("P2");
        reject();
    });
};


const p3 = () => {
    return new Promise((resolve, reject) => {
        console.log("P3");
        resolve();
    });
};

p1().catch(() => {
    console.log("Caught p1");
    return stop; // don't resolve
}).then(p2).catch(() => {
    console.log("Caught p2");
    return stop;
}).then(p3).catch(() => {
    console.log("Caught p3");
    return stop;
}).then(() => {
    console.log("Final then");
});
trincot
  • 317,000
  • 35
  • 244
  • 286
  • `new Promise(resolve => null)` will just halt the entire promise chain. I'm not sure that's a very good idea, as it makes any sort of recovery later impossible. Even something like attaching a `.finally()` is not going to be processed. I'd suggest either not attaching a `.catch()` if no recovery attempt should be made because knowing a promise chain failed is usually more valuable than just leaving it in limbo forever. An alternative is to reject from within the `.catch()` either by throwing an error or returning a rejected promise. That still fails the rest of the chain. – VLAZ Mar 01 '21 at 21:30
  • Indeed it halts the entire promise chain. That was my understanding of the OP's wishes. I would never do this myself, but then I also don't want things to halt like that. – trincot Mar 01 '21 at 21:53