Why How to Not Throw Inside a Promise.Catch Handler

Rethrowing error in promise catch

There is no point to a naked catch and throw as you show. It does not do anything useful except add code and slow execution. So, if you're going to .catch() and rethrow, there should be something you want to do in the .catch(), otherwise you should just remove the .catch() entirely.

The usual point for that general structure is when you want to execute something in the .catch() such as log the error or clean up some state (like close files), but you want the promise chain to continue as rejected.

promise.then(function(result){
//some code
}).catch(function(error) {
// log and rethrow
console.log(error);
throw error;
});

In a tutorial, it may be there just to show people where they can catch errors or to teach the concept of handling the error, then rethrowing it.


Some of the useful reasons for catching and rethrowing are as follows:

  1. You want to log the error, but keep the promise chain as rejected.
  2. You want to turn the error into some other error (often for easier error processing at the end of the chain). In this case, you would rethrow a different error.
  3. You want to do a bunch of processing before the promise chain continues (such as close/free resources) but you want the promise chain to stay rejected.
  4. You want a spot to place a breakpoint for the debugger at this point in the promise chain if there's a failure.
  5. You want to handle a specific error or set of errors, but rethrow others so that they propagate back to the caller.

But, a plain catch and rethrow of the same error with no other code in the catch handler doesn't do anything useful for normal running of the code.

Can't catch error thrown in promise using promises 'catch'

Your comment on my comment suggested the case was different from the other answer. I can't really fit this explanation in a comment, so:

The catch block will not wait for the promise to reject, actually. Try this:

const myPromise = new Promise(() => {
console.log('inside of the promise');
setTimeout(() => { console.log('inside of the timeout function'); throw new Error('ERROR') }, 1000);
}).catch(err => {
console.log('CATCHED ', err);
});
console.log('outside of the catch block, and my promise is', myPromise);

You will find that the outside of the catch message is written to the console prior to the message from inside the timeout function. At that point, the promise is unresolved (pending).

When you use setTimeout, the function gets added to the end of JavaScript's queue.

EDIT

You can get around this issue by calling reject on the promise.

const myPromise = new Promise((resolve,reject) => {
setTimeout(() => { reject('ERROR'); }, 1000);
}).catch(err => {
console.log('CAUGHT ', err);
});

This is different because in this case the reject function is kept in a scope bubble (closure), so it is still able to resolve.

EDIT 2

I did find another question that has some discussion in the comments about this:

JavaScript Promises - reject vs. throw

From this, I would revise my explanation.

First, it's helpful to keep catch blocks separate in your mind from the catch function you chain onto a promise. Think of the promise as catching a thrown error with a built in invisible catch block. In handling it the promise's catch block will call 'reject' which will trigger the catch function.

Thrown error => promise's invisible (implicit) catch block => reject() => promise's catch() function

It can't catch it, though, if the error is thrown from a different call stack, which it is with setTimeout.

Thrown error (in another call stack from setTimeout) => uncaught exception

The chain stops there, because the exception is bubbling up in a different call stack.

But YOU can still call reject, triggering the catch function, which is what the code snippet above does.

reject => promise's catch() function

It's a shorter chain of events, cutting right to the behavior you want -- the triggering of the code in the catch function.

Throwing inside a catch block in a nested Promise to trigger the catch block of outer Promise, is there an alternative cleaner way?

I think the clean way to do this would be using async/await. But before going there, is your question how to not run the outer promise when the inner promise fails?

The example below:

  • When the inner promise rejects, the chain stops.
  • When the outer promise rejects, the inner is already fulfilled.

const fun42 = () => {    return new Promise((resolve, reject) => {        setTimeout(() =>{            resolve(42)            reject('something at fun 42 went wrong')        }, 500)    })}
const fun32 = () => { return new Promise((resolve, reject) => { setTimeout(() =>{ //resolve(32) reject('something at fun 32 went wrong') }, 500) })}
fun32().then(x => { console.log(x) return fun42()}).then( y => { console.log(y)}).catch (e => { console.log(e)})

JavaScript Promises - reject vs. throw

There is no advantage of using one vs the other, but, there is a specific case where throw won't work. However, those cases can be fixed.

Any time you are inside of a promise callback, you can use throw. However, if you're in any other asynchronous callback, you must use reject.

For example, this won't trigger the catch:

new Promise(function() {
setTimeout(function() {
throw 'or nah';
// return Promise.reject('or nah'); also won't work
}, 1000);
}).catch(function(e) {
console.log(e); // doesn't happen
});

Why doesn't the try catch block catch the promise exception?

I can see two possible aspects to your question:

  1. The error being thrown in test is in the synchronous (not asynchronous) part of test. Why is it a promise rejection rather than a synchronous exception?

  2. The promise from test() is being rejected, why doesn't catch catch that rejection?

#1 - Why is it a rejection?

Because even if an async function throws during the synchronous portion of its work, that just rejects the promise it returns, it doesn't raise a synchronous error. It's just a design decision made during the design of async functions, and a smart one — if it were a synchronous error when throwing in the synchronous part of the function but a promise rejection after that, it would be chaotic and hard to understand. So it's simple: throwing in an async function always rejects its promise.

(This is consistent with how the promise constructor treats the executor callback you pass into it. When you do new Promise((resolve, reject) => /*...*/}), the promise constructor calls your function synchronously, but if you throw during the call, it uses what you throw to reject the promise rather than allowing it to continue as a synchronous exception.)

#2 - Why isn't rejection caught by the catch block?

Because you're not using await. Promise rejections are only exceptions when you are awaiting the promise. If you use the promise methods then/catch/finally to attach handlers, rejection is handled by calling the rejection handler, not by the exception mechanism.

So either use promise methods to attach fulfillment and rejection handlers:

test()
.then(result => {
// ...use `result` here...
})
.catch(error => {
// ...handle/report error here...
});

Or use await in an async function (or at the top level of a module if you have top-level await in your enviroment):

// In an `async` function (or the top level of a module in cutting-edge environments)
try {
const result = await test();
// ...use `result` here...
}
catch(err){
// ...handle/report error here...
}

promise catch: how to know if the error comes from promise rejection or from the then statement

then accepts two parameters¹: A function to call on fulfillment, and another to call on rejection. So you can handle the initial rejection earlier:

fetch(/*...*/)
.then(
// Fulfillment handler
(response) => {
/*...*/
},
// Rejection handler for `fetch` promise
(error) => {
/*...*/
}
)
.catch((error) => {
// This rejection handler only sees errors not handled above
});

The first rejection handler (in the then call) is only called for rejections of the promise from fetch, not rejections caused by errors in the fulfillment handler.

The second rejection handler (in the catch call) is called for rejections caused by errors (or rejected promises) from the handlers that come before it (either fulfillment or rejection); it doesn't get called if the original promise is rejected (but it may well get called if the first rejection handler throws an error or returns a promise that is/will be rejected).

So in those cases where you care, you can handle it closer to the source.

Note that all rejection handlers that don't either throw an error or return a promise that is/will be rejected convert rejection (of the original promise) into fulfillment (of the one from then/catch). That can matter for downstream then handlers.


A key thing to understanding this is to remember that then (and its wrappers catch and finally) return a new promise. That's why:

aPromise.then(onFulfilled).catch(onRejected);

is different from

aPromise.then(onFulfilled, onRejected);

The first one hooks up a fulfillment handler to aPromise, and then hooks up a rejection handler to the promise then returns (which means the rejection handler will be called if aPromise rejects or if the fulfillment handler throws an error or returns a promise that rejects). In contrast, the second one only hooks up handlers on aPromise, so errors/rejections from the fulfillment handler do not trigger the rejection handler.


¹ In fact, .catch(fn) is just a wrapper for .then(undefined, fn). :-) spec link

How to handle catch blocks and then of Promise

The key thing here — and one of the key things about using promises — is that then and catch create new promises. So the promise that was rejected that wasn't handled was the one created by then:

let promise = request(false);

promise.then( response => { // Creates new promise, rejection isn't handled
console.log('response' , response);
});

promise.catch( (err) => { // Creates new promise
console.log('got Error', err);
});

This is one of the reasons you see promise chains:

request(false)
.then( response => {
console.log('response' , response);
})
.catch( (err) => {
console.log('got Error', err);
});

There, three promises are still created (the original from request, the one from then, and the one from catch), but rejection is handled for all three of them by the final catch handler.

The promise created by then and catch work like this:

  • If the underlying promise resolves:

    • If there's no then handler, resolve with the resolution from the original promise
    • If there's a then handler, call it:

      • If the handler returns a thenable (a promise-like object), hook up to it and resolve or reject based on whether that thenable resolves or rejects
      • If the handler returns a non-thenable value, resolve with that value
      • If the handler throws an error, reject with that error
  • If the underlying promise rejects:

    • If there's no catch handler, reject with the rejection from the original promise
    • If there's a catch handler, call it and do exactly what's done with the then handler above (resolve or reject based on what it returns or throws)

Why a promise reject is not catched within a try catch block but produces an Uncaught error?

Technical reasons behind:

HostPromiseRejectionTracker is an implementation-defined abstract
operation that allows host environments to track promise rejections.

An implementation of HostPromiseRejectionTracker must complete
normally in all cases. The default implementation of
HostPromiseRejectionTracker is to unconditionally return an empty
normal completion.

https://www.ecma-international.org/ecma-262/10.0/index.html#sec-host-promise-rejection-tracker

Basically javascript engines can freely implement this spec. In the case of browsers you don't get the Error captured inside the try/catch because the error is not emitted where you think it should be. But instead it's tracked with this special event that throws the error in the console.
Also on nodejs, this event causes the process to exit if you have node set to exit on unhandled exceptions.

On the other side, if you instead use async/await, the rejection is treated like an 'error' in practical terms. Meaning that the newer async/await feature behaves in a different fashion showing that it is not only syntactic sugar for Promises.

https://tc39.es/ecma262/#sec-throwcompletion

In sum, if you use Promise.then you are forced to provide a reject argument or chain it with .catch to have the rejection captured or else it will reach the console and in case of nodejs to exit the process if configured to do so (I believe new nodejs does this by default).
But if you use the newer async/await syntax you not only have a concise code (which is secondary) but a better rejection handling because it can be properly nested in a try/catch and it will behave like an standard Error.



Related Topics



Leave a reply



Submit