5

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

Liam
  • 27,717
  • 28
  • 128
  • 190
Reed Sandberg
  • 671
  • 1
  • 10
  • 18
  • is `si.osInfo()` returning a promise? – Adarsh Mohan Mar 10 '21 at 21:41
  • Maybe `si.osInfo` stops waiting on any asynchronous events that would keep the event loop alive, but fails to resolve the promise that would let your code continue. Sounds like a bug in that library to me. – Bergi Mar 10 '21 at 21:44
  • If the promise is not resolved, wouldn't the `await` operator keep waiting until it resolves or a timeout is encountered? Why would it just suddenly exit that function stack frame? – Reed Sandberg Mar 10 '21 at 22:52
  • 2
    The `await` operator doesn't "wait" with the frame still on the call stack, it suspends the execution and clears the call stack, every time. It would resume the function when the promise is settled (just like a `then` handler) - which never happens in your case. The handler on the promise doesn't prevent nodejs from exiting the event loop if there are no running asynchronous tasks left. – Bergi Mar 10 '21 at 23:30
  • So within a running node process is it safe to say there is another 3rd outcome of a promise other than resolution/rejection? If this 3rd outcome is encountered, the promise/event loop may suddenly/silently terminate and cancel any subsequent then/catch/finally code of that promise? If so, is there any way to detect or handle this 3rd outcome? – Reed Sandberg Mar 11 '21 at 00:31
  • Use .then()/.catch() instead of 'wait'. 'Wait' is syntax sugar that is not exactly what is expected. – Riad Baghbanli Mar 11 '21 at 00:33
  • @RiadBaghbanli, same exact behavior using promises with then/catch as with async/await - the problem lies deeper than those semantics. – Reed Sandberg Mar 11 '21 at 00:47
  • I suspect Bergi's diagnosis is correct. I wouldn't call it a "third outcome". I'd say it's the exact same outcome as if you went to your computer's process manager and force-quit the process. If nothing keeps the process alive, it will die, and dead processes don't run fulfillment or rejection handlers. – Domenic Mar 11 '21 at 01:36
  • I checked the code of `osInfo` and it does not reject code on errors. Even the errors are resolving. So I don't think it will ever reach to your catch block. And this `osInfo` method accept a callback which execute before resolve the promise. Did you try to add that callback and see if something happens ? Here line 193 - https://github.com/sebhildebrandt/systeminformation/blob/master/lib/osinfo.js – Dilshan Mar 11 '21 at 01:42
  • @ReedSandberg It's not a third outcome. It's *no* outcome. And it's not "allowing" the event loop to terminate - the event loop simply never cares about promises and terminates normally as it always would if there are no running asynchronous tasks left. (Notice that a promise, and by extension an `async function`, is not a "task" here - it's just an elaborate callback mechanism). – Bergi Mar 11 '21 at 02:04
  • 2
    @Dilshan Indeed, a [classic `Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it) in [here](https://github.com/sebhildebrandt/systeminformation/blob/41b3059f216ad0aefece4fbffd3a6c2fe829e71b/lib/osinfo.js#L246-L255) – Bergi Mar 11 '21 at 02:08
  • Some good points raised here and helpful input, but the core issue remains unresolved. I've updated the question and left out some of the finer details that drew attention but were not germane to the central question, which asks why a `finally` block might not be reached (other than the usual suspects which have been ruled out). – Reed Sandberg Mar 11 '21 at 07:12
  • You need to answer one critical point, does `someAsyncFunc` completes? finally will only execute after `someAsyncFunc` is resolved(success/error). – Mat J Mar 11 '21 at 07:21
  • Yes, `si.osInfo()` (or `someAsyncFunc`) will _eventually_ complete and resolve the promise. Looks like the issue here is that the body of the function used in the `Promise` returned by `si.osInfo()` may complete before the promise is resolved/rejected due to the mixing of async callbacks and promises, which are not compatible. But to answer the main question, indeed one circumstance where a finally block will not be reached is if the promise function completes without resolving/rejecting anything. – Reed Sandberg Mar 11 '21 at 09:44

2 Answers2

6

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.

Reed Sandberg
  • 671
  • 1
  • 10
  • 18
2

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 the try-block and catch-block(s) execute, but before the statements following the try...catch...finally-block. Note that the finally-block executes regardless of whether an exception is thrown. Also, if an exception is thrown, the statements in the finally-block execute even if no catch-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.

enter image description here

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

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
  2. https://tc39.es/ecma262/#sec-promise.prototype.finally
  3. https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md
Dilshan
  • 2,797
  • 1
  • 8
  • 26
  • "*What if the above isUefiLinux never resolved ?*" - that's not the problem - or, not the caller's problem; it is expected that a promise eventually is settled same like an async callback is expected to be called at some point. The real problem is with `isUefiLinux()` rejecting the promise, which is not handled anywhere but ignored and will not cause the `osInfo` promise to be settled. – Bergi Mar 11 '21 at 18:56
  • @Bergi, yes that area of the code can be problematic generally speaking, but wasn't the issue in this particular case, since I was running on mac (darwin). – Reed Sandberg Mar 11 '21 at 19:58
  • @Bergi That's what I meant. `isUefiLinux` does not handle exceptions. So technically the only thing that available to run next in that particular code is the `then` block. If that also not called ( That mean not resolved by not calling resolve or because of an exception or whatever reason ) then what happens ? That's what I was asked. – Dilshan Mar 12 '21 at 02:36