Unsettled promises and how to handle them

Background and assumptions:

Consider that a promise is "complete" when its promisified (or wrapped) function exits or returns (line 10 below). Consider a promise as "settled" if it is resolved (or fulfilled, line 4 below), or rejected (line 6 below) by the time it completes.

Everyone is familiar with these terms, and all is good so far. However there is yet another class of outcomes that is not well documented or widely discussed - unsettled promises. Consider a promise as "unsettled" if it completes, but has neither been resolved nor rejected (reaches line 8 below). A completed promise may be fulfilled or rejected later (sometime after completion) through asynchronous means, but if it was not fulfilled/rejected by the time it completed, it is nonetheless considered "unsettled".

 1 prepareNextSearch(): Promise<PrepStatus> {
 2   return new Promise((resolve, reject) => {
 3     if (this.canForp()) {
 4       resolve('I can forp');                    <====== Resolved
 5     } else if (this.canRarp()) {
 6       reject('I cannot forp but I can rarp');   <====== Rejected
 7     } else {
 8       console.log('I cannot promise anything'); <====== Unsettled
 9     }
10   });                                           <====== Completed
11 }

So consider the following outcomes for a completed promise (not to be confused with possible states of a promise):

  • Resolved (via line 4)
  • Rejected (via line 6)
  • Unsettled (via line 8)

The problem:

Currently, an unsettled promise will silently ignore any code in any subsequent chained promise attachments (then/catch/finally), or analogously, code that comes after an await in an async function and all outer async functions in a call-stack. In other words, unsettled promises will suddenly and silently break from the logical thread of execution with no way to catch them.

Example of problematic behavior of an unsettled promise (from prepareNextSearch) using the Promise pattern (same results with the async/await pattern):

 1
 2 youllBeSorry() {
 3   const nextSearch = this.prepareNextSearch()
 4     .then(promised => {
 5       console.log(promised); //  <=========== NOT REACHED!
 6     }).catch(error => {
 7       this.prepAltSearch();  //  <=========== NOT REACHED!
 8     }).finally(() =>
 9       console.log(           //  <=========== NOT REACHED!
10        'Whew! The search preparation completed one way or another.'
11       );
12     });
13
14   this.doOtherStuffWhileSearching();
15 }

If run with no capability for the promise of this.prepareNextSearch() to "forp" or "rarp" (for the sake of argument), lines 5, 7 and 9 will not execute - indeed, even finally will be silently skipped over. The string "Whew! The search preparation completed one way or another." will never be logged, and the program will happily carry on at line 14 as if nothing was ever promised, and no prep will be done.

This behavior is not well-documented, and for the vast majority of cases, unexpected and undesired. Many would consider it counterintuitive to the semantics of finally blocks (at the least), which ultimately never runs. Further, there is no prescribed way to catch and handle these exceptional cases.

"A pending promise can either be fulfilled with a value or rejected with a reason (error)."
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Proposal 1:
The first proposal is for Promise to throw an "UnsettledPromiseError" (or similar) when an unsettled promise occurs, providing a way to catch and handle these exceptional cases.

Proposal 2:
An alternate proposal is to provide built-in semantics to catch and handle unsettled promises as first-class citizens with an "unsettled" handler within Promise.then() and optionally a corresponding "unsettled" block.

Examples:

class ExploreTheUnknown {
  /**
   * Prepare a search with algo1 (falling back to algo2), execute the search
   * and then clean up.
   */
  searchExoplanets(): Promise<Exoplanet[]> {
    return this.prepareAlgo1Search()
      .catch(error => {
        console.warn(`Search prep algo 1 failed, fallback to algo 2: ${error}`);
        return this.prepareAlgo2Search();
      })
      .then(() => this.findExoplanets())
      .finally(() => {
        console.log('The search has ended.');
        this.freeResources();
      });
  }

  freeResources(): void {
    this.deleteWorkingDir();
  }

  prepareAlgo1Search(): Promise<void> {
    return new Promise((resolve, reject) => ...);
  }

  prepareAlgo2Search(): Promise<void> {
    return new Promise((resolve, reject) => ...);
  }

  findExoplanets(): Promise<Exoplanet[]> {
    return new Promise((resolve, reject) => resolve(['LHS 3844b']));
  }

If prepareAlgo1Search() fails for any reason, we want to run the less optimal but still acceptable prepareAlgo2Search() method. In all cases, we need to perform some cleanup after our search or we'll have a major resource leak on our hands. With current behavior, our search for the unknown might never be conducted depending on the outcome of prepareAlgo1Search(), and resource leaks could very well happen eventually.

With proposal 1, the code would run as expected with no changes and would conduct our search (unless both algos fail) and always clean up afterwards, regardless (under normal operating conditions). We could demonstrate proposal 1 further and report on any underlying issues like so:

  searchExoplanetsWithUnsettledException(): Promise<Exoplanet[]> {
    return this.prepareAlgo1Search()
      .catch(error => {
        if (error instanceof UnsettledPromiseError) {
          console.warn('Search prep algo 1 is unsettled (possible bug in algo 1), fallback to algo 2.');
        } else {
          console.warn(`Search prep algo 1 failed, fallback to algo 2: ${error}`);
        }
        return this.prepareAlgo2Search();
      })
      .then(() => this.findExoplanets())
      .finally(() => {
        console.log('The search has ended.');
        this.freeResources();
      });
  }

Proposal 2 might look something like:

  searchExoplanetsWithUnsettledHandler(): Promise<Exoplanet[]> {
    return this.prepareAlgo1Search()
      .then(
        // Resolved handler.
        () => undefined,
        // Error handler.
        (error: any) => {
          console.warn(`Search prep algo 1 failed, fallback to algo 2: ${error}`);
          return this.prepareAlgo2Search().unsettled(() => {
            throw new Error('Search prep algo 2 is unsettled (possible bug in algo 2), giving up on search prep.');
          });
        },
        // Unsettled handler.
        () => {
          console.warn('Search prep algo 1 is unsettled (possible bug in algo 1), fallback to algo 2.');
          return this.prepareAlgo2Search().unsettled(() => {
            throw new Error('Search prep algo 2 is unsettled (possible bug in algo 2), giving up on search prep.');
          });
        }
      )
      .then(() => this.findExoplanets())
      .finally(() => {
        console.log('The search has ended.');
        this.freeResources();
      });
  }

Proposal 1 seems cleaner with more of what one might expect from try/catch/finally semantics. Proposal 2 is more complex, a bit more redundant, but perhaps more flexible and backwards-compatible with the current behavior (albeit behavior that is unexpected and undesired in most cases).

The promise (no pun intended) of curtailing programming errors with Promises is mostly fulfilled, but still needs to address this important class of outcomes.

"Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors."
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

How can you possibly know when an unsettled promise occurs? A promise might settle 10 seconds, or 10 years after creation.

The solution here is to use Promise.race, with your own timeout value that makes sense for your use case.

1 Like

Just because the callback finished running does not mean the promise is unsettled. It's not trivial for a browser to tell whether or not a promise will settle or not (if it is even possible at all). For example:

const pause = async duration => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(); // <===== not reached until after duration
        }, duration);
    }); // <==== Completed? Unsettled...?
}

How would you catch these cases?

As others have pointed out, trying to calculate this would not be possible, and would be just as unsolvable as the halting problem.

Your undesirable effect of the finally block never executing also exists without promises. Consider this:

try {
  while (someConditionThatMayNeverBeFalse()) {
    doSomething()
  }
} finally {
  // potentially never executes
}

The best one can do is to cause these things to time out. For example:

const doTask = ({ timeout = 10000 }) => new Promise((resolve, reject) => {
  const interval = setInterval(() => {
    if (someConditionThatMayNeverBeFalse()) return
    clearInterval(interval)
    resolve()
  }, 100)
  setTimeout(() => reject('Timed out!'), timeout)
}

Appreciate the comments - very helpful in further troubleshooting a node.js issue that prompted me to post this idea.

As @vrugtehagel points out, a function wrapped in a promise can indeed return/exit without being "completed" as I've defined above.

So with that assumption corrected, there is really no need to differentiate between "settled" and "unsettled" promises - at least from the perspective of the ES standard. We have only "settled" and "pending" promises as documented, with settled promises being either fulfilled or rejected. A pending promise may very well never settle, at least not before the application that uses it, or its process terminates. A promise that never settles may be intentional or unintentional, but I think async functions help avoid programming errors for unintentional cases (kind of like futures in other languages). Semantically, it seems a promise should represent an object or value that is intended to be known and useful at some point.

That said, it feels like there are some missing elements in dealing with the concept of promises and async behavior as compared with other languages. A common feature that might have helped avoid the specific issue I've been dealing with in node.js, is having clear and intuitive semantics for waiting on promises (at any level, particularly the top level in a script), with an optional timeout. Perhaps there are limitations specific to node's implementation and not the language itself. Or perhaps I'm missing something obvious with await for example, but the only advice I could find was wrapping setTimeout in a promise, or racing promises with a timeout. Neither of which were intuitive, or even helped in this case.

Any thoughts on ES features (existing or proposed) related to waiting for promises in terms of absolute timing and synchronization (using timeouts, etc)?

That’s precisely what Promise.race is for (you’d race your pending promise against one that wraps setTimeout).

Understood that Promise.race/setTimeout could be used for this, but seems that Promise.race is a more of a general operation available to determine the outcome of a group of promises based on which one settles first, rather than specifically to await the value of a promise with an optional timeout.

Since this is a common operation of async programming, I'm suggesting ES provide this operation as a built-in, documented and intuitive feature of the language.

Currently, with Promise.race:

    const configPromise: Promise<ConfigStatus> = this.configureApplication();
    const configTimeoutRacer: Promise<void> = new Promise((resolve, reject) => {
      setTimeout(() => reject(new ConfigTimeoutError()), CONFIG_MAX_WAIT_TIME);
    });
    // Wait until configuration is complete or timeout.
    const configStatus: ConfigStatus = await Promise.race([configPromise, configTimeoutRacer]);
    if (configStatus.greenLight) {
      runApplication();
    }

I'm proposing something like:

    const configStatus = this.configureApplication().await(CONFIG_MAX_WAIT_TIME);
    if (configStatus.greenLight) {
      runApplication();
    }

Where (much like the built-in await keyword) Promise.prototype.await() waits until the promise resolves and returns its value. If the promise is rejected, it throws the rejection. It also takes an optional timeout parameter, which if exceeded, will throw a timeout exception.

The language has no concept of "time", really - setTImeout isn't part of the language - so it's not something the spec can include. The intention was very much for you to compose Promise.race and setTimeout yourself to create this abstraction.

I agree this element of timing is missing from the language. Seems to be an important one though - is it worth a proposal to add it?

btw - what other languages do you have in mind when you're talking about how they have some nice async features you're wishing javascript had? What do their implementations look like?

I mostly had in mind JVM-based languages, which have wait/timeout similar to my previous comment (Promise.prototype.await). Kotlin has withTimeout that can be used similarly. Python has similar semantics with asyncio.

Others have encountered similar issues using node.js that I think can (and if so, should) be addressed with some similar semantics and features in the language spec itself: