Thanks to all the comments I was able to troubleshoot the core issue and find a satisfactory answer.
In short, yes - other than the usual suspects, a Promise.finally
(or try/finally with await) block may never be reached and executed if the promise in question never "settles". Some promises may take much longer to settle than expected (I/O wait, etc) and eventually resolve or reject, but if a promise never settles (by bug or by design) all code depending on that promise (then
, finally
, etc) will never execute.
So what is an unsettled promise, and how do they occur? A promise may never settle if its resolve
or reject
callback functions are never called (by bug or by design).
Consider the following promise:
1 iPromiseTo(): Promise<any> {
2 return new Promise((resolve, reject) => {
3 if (this.willNeverHappen()) {
4 resolve('I can forp'); <====== Resolved
5 } else if (this.willAlsoNeverHappen()) {
6 reject('I cannot forp but I can rarp'); <====== Rejected
7 } else {
8 console.log('I cannot promise anything'); <====== Unsettled
9 }
10 });
11 }
If the promise function body completes without resolving/rejecting (to line 10 via line 8) any dependent code including then/catch/finally blocks attached later (see below) will simply not execute and will be silently ignored. In most cases, this is probably considered a bug, which is why async
functions are advantageous in most situations. An async
function will always either resolve or reject when its function body completes (its function body may never complete, but that's a different topic).
Using this flawed promise within Promise
semantics:
1
2 youllBeSorry() {
3 this.iPromiseTo()
4 .then(promised => {
5 console.log(promised); // <=========== NOT REACHED!
6 }).catch(error => {
7 console.log(error); // <=========== NOT REACHED!
8 }).finally(() =>
9 console.log('whew!'); // <=========== NOT REACHED!
10 });
11 }
Lines 5, 7 and 9 will not execute.
Using it within async/await semantics:
1
2 async youllBeSorry() {
3 try {
4 const promised = await this.iPromiseTo();
5 ... // <=========== NOT REACHED!
6 } catch(error) {
7 console.log(error); // <=========== NOT REACHED!
8 } finally {
9 console.log('whew!'); // <=========== NOT REACHED!
10 }
11 }
Lines 5, 7 and 9 will not execute and the function will silently return at line 4 without error or any subsequent exception handling.
Another class of promises that may never settle are those with function bodies that never complete due to an infinite loop or infinite hang, etc (by bug or by design). These effectively behave similarly to those promises that complete their function body but never call resolve
or reject
, although there are probably other differences between the two that aren't covered here.
Ok, so how can I handle these "unsettled" promises? Unfortunately there is no prescribed way to catch and handle this special outcome of promises (at the time of this writing). There is some advice, but I haven't tried it myself:
await/async how to handle unresolved promises
There is top-level await
that might already be available for you, or coming soon (also see its older IIFE workaround counterpart):
https://v8.dev/features/top-level-await
Ideally, ECMAScript would provide more top-level control over async behavior - see kotlin (or even python) coroutines for a more robust example of how to do async better.
Finally (no pun intended), I'll note that the reason the finally
block usually executes as expected in this particular case is that the oclif framework is being used, which unfortunately has exit
calls embedded in its API routines. In all cases, the promise here in question can be fulfilled/settled, but it races with the exit call in oclif so is not always resolved and dependent code not always run before the exit exception is thrown and node exits. So turned out to be one of the "usual suspects" in this particular case, but troubleshooting led to discovery of these shortcomings in ECMAScript.