Marc
Marc

Reputation: 3914

Promise chain continues after rejection

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.

Upvotes: 3

Views: 3532

Answers (4)

VLAZ
VLAZ

Reputation: 29087

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.

Upvotes: 4

trincot
trincot

Reputation: 351084

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");
});

Upvotes: 0

Braden Hancock
Braden Hancock

Reputation: 78

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);
})

Upvotes: 1

Valery
Valery

Reputation: 315

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).

Upvotes: 0

Related Questions