Reputation: 741
Other than the usual suspects (process.exit()
, or process termination/signal, or crash/hardware failure), are there any circumstances where code in a finally block will not be reached?
The following typescript code usually executes as expected (using node.js) but occasionally will terminate immediately at line 4 with no exceptions being raised or change in the process exit code (exits 0/success):
1 import si from 'systeminformation';
2 async populateResolvedValue() {
3 try {
4 const osInfo = await si.osInfo();
5 ...
6 } finally {
7 console.log('whew!'); // <=========== NOT REACHED!
8 }
9 }
I've verified this within an IJ debug session - the finally
block on line 7 occasionally will not execute and will terminate immediately at line 4 (with some unwinding of the stack). The only case I know of where this could happen (and still exit successfully) is if a segfault is encountered somewhere within someAsyncFunc()
, but I added "segfault-handler" and nothing showed up there.
I have also tried this using Promise.then/finally rather than async/await with try/finally semantics - same exact behavior.
node.js: v12.18.2 and v14.16.0
Upvotes: 7
Views: 7751
Reputation: 741
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.
Upvotes: 7
Reputation: 3001
Other than the usual suspects (process.exit(), or process termination/signal, or crash/hardware failure), are there any circumstances where code in a finally block will not be reached?
If the promise will resolve or reject in future then it should reach to the final block.
According to the MDN docs,
The
finally
-block contains statements to execute after thetry
-block andcatch
-block(s) execute, but before the statements following thetry...catch...finally
-block. Note that thefinally
-block executes regardless of whether an exception is thrown. Also, if an exception is thrown, the statements in thefinally
-block execute even if nocatch
-block handles the exception.
A promise is just a JavaScript object. An object can have many states. A promise object can be in pending
state or settled
state. The state settled
can divide as fulfilled
and rejected
. For this example just imagine we have only two state as PENDING
and SETTLED
.
Now if the promise never resolve or reject then it will never go to the settled
state which means your then..catch..finally
will never call. If nothing is reference to the promise then it will just garbage collected.
In your original question you mentioned about a 3rd party async method. If you see that code, the first thing you can see is, there are set of if(..)
blocks to determine the current OS.
But it does not have any else
block or a default case.
What if non of the if(..)
blocks are trigger ? There is nothing to execute and you already returned a promise with return new Promise()
. So basically if non of the if(..)
blocks are triggered, the promise will never change its state from pending
to settled
.
And then as @Bergi also mentioned there are some codes like this. A classic Promise constructor antipattern as he mentioned. For example see the below code,
isUefiLinux().then(uefi => {
result.uefi = uefi;
uuid().then(data => {
result.serial = data.os;
if (callback) {
callback(result);
}
resolve(result);
});
});
What if the above isUefiLinux
never settled ? Again then
won't trigger on isUefiLinux
and never resolve the main promise.
Now if you check the code of isUefiLinux
it is resolving even it throws an error.
function isUefiLinux() {
return new Promise((resolve) => {
process.nextTick(() => {
fs.stat('/sys/firmware/efi', function (err) {
//what if this cb never called?
if (!err) {
resolve(true);
} else {
exec('dmesg | grep -E "EFI v"', function (error, stdout) {
//what if this cb never called?
if (!error) {
const lines = stdout.toString().split('\n');
resolve(lines.length > 0);
}
resolve(false);
});
}
});
});
});
}
But there are two callback functions in the isUefiLinux
method, a mix of promises and callbacks;a 'hell'. Now what if these callbacks are never called ? Your promise will never resolves.
I've verified this within an IJ debug session - the finally block on line 7 occasionally will not execute and will terminate immediately at line 4
"Occasionally" will not execute ? Isn't that the above explanation explain this for some level ?
More Information
Upvotes: 2