I am trying to understand promise error handling for the cases of thrown errors and exceptions (code bugs) in the promise executor, comparing them to reject. This is for node.js.
It seems like there are clear differences in the handling of reject vs throw and exceptions.
Some web articles imply there is an implicit try/catch around promises that converts exceptions and throws to rejects, but apparently that is not true, or not always true.
I wrote some test code to study it. That code and the results are below.
Test code:
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(value => {
resolve();
}, ms);
});
}
function f(delayMs, action) {
return new Promise((resolve, reject) => {
if (delayMs == 0) {
console.log(`f() forcing error type ${action} sync...`);
if (action == 'exception') {
console.xxx();
} else if (action == 'throw') {
throw new Error('thown error sync');
} else {
reject('reject sync');
}
} else {
delay(delayMs).
then(() => {
console.log(`f() forcing error type ${action} after delay...`);
if (action == 'exception') {
console.yyy();
} else if (action == 'throw') {
throw new Error('thrown error after delay');
} else {
reject('reject after delay');
}
});
}
});
}
function main(delayMs, action) {
f(delayMs, action).
then(value => {
console.log('Should not see this');
}).
catch(err => {
if (typeof err == 'object') console.log('In main catch:', err.message);
else console.log('In main catch:', err);
}).
finally(() => {
console.log('In main finally');
});
}
main(0, 'exception');
main(0, 'throw');
main(0, 'reject');
main(500, 'reject');
main(1000, 'exception');
main(1500, 'throw');
Output:
f() forcing error type exception sync...
f() forcing error type throw sync...
f() forcing error type reject sync...
In main catch: console.xxx is not a function
In main catch: thown error sync
In main catch: reject sync
In main finally
In main finally
In main finally
f() forcing error type reject after delay...
In main catch: reject after delay
In main finally
f() forcing error type exception after delay...
/mnt/c/Users/test/promise-stackoverflow-2.js:25
console.yyy();
^
TypeError: console.yyy is not a function
at /mnt/c/Users/test/promise-stackoverflow-2.js:25:33
Node.js v18.12.1
For the synchronous case, all three error types are handled the same, being caught in the catch in the main function.
For the asynchronous case, which admittedly involves another Promise, the reject case is still handled in the catch in the main function. But the other two cases (exception and throw) cause a higher level exception and the process exits.
It looks like I can add a catch handler for the delay() promise, and then do a reject in that catch and it will propagate to the main catch handler. I didn’t think I would need to do this – am I missing something?
Also, some articles imply that error handling is easier using async/await. To me, it seemed like if you want to do strict error handling, just using promises was easier.
2
Answers
When you do
delay().then()
and then throw inside the.then()
, that throw is caught by the.then()
and turned into a rejected promise. That throw does not make it to thetry/catch
of the executor. Then, you don’t have any code to catch or handle that rejection and thus you get an unhandled rejection.There is an automatic
try/catch
around the promise executor, but that only catches thrown exceptions at the top level of that function, not unhandled promise rejections.It’s not really instructive to discuss how to fix this structure of code because you really shouldn’t be doing
fn().then()
inside a promise executor anyway. You are manually wrapping a promise with another unnecessary promise at that point. The outer, manually created promise would be unnecessary.Yes, that will add the missing reject handler. But, this shouldn’t normally happen in real world coding because you shouldn’t be doing
fn().then()
inside a promise executor in the first place.With some experience, you will find that coding with promises, in general, is simpler with
async/await
, particularly when you get to more complicated scenarios such as multiple sequenced asynchronous operations and branching of asynchronous operations. A single chainof operations can be done easily either way.
Error handling is sometimes simpler with
async/await
and sometimes not. It really depends upon the situation and how local/global you want to handle the error. Handling a rejection locally (directly on the immediate promise) is sometimes easier with.catch()
whereas centralizing reject handling, particularly when there are multiple promise chains involved may be a lot simpler withasync/await
andtry/catch
. It really depends upon the layout of the code and where you need to catch/handle the error.Plus, the automatic catching of synchronous exceptions and auto-turning them into rejections can be useful in
async
functions too so you don’t have to code to handle both synchronous exceptions and rejected promises in cases where either could occur.The call to
delay()
has broken the promise chain. The only thing tying the promise created bydelay
back to the parent is the closure onreject
from the parent promise scope.As jfriend mentioned, this isn’t really how you would want to write promise code but to demonstrate, the promise would need to be resolved from
f
to be handled bymain