Reed Sandberg
Reed Sandberg

Reputation: 741

Under what circumstances would a finally block not be reached?

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

Answers (2)

Reed Sandberg
Reed Sandberg

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

Dilshan
Dilshan

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 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

Upvotes: 2

Related Questions