Gaurab Paul

Polyglot software developer & consultant passionate about web development, distributed systems and open source technologies

Support my blog and open-source work

Tags

Javascript Async/Await with arbitrary Thenables
Posted  6 years ago

Usage of C# inspired async/await syntax (Now a stage 4 proposal) is now fairly mainstream in javascript, and native support is available in major browsers and Node.

Most of the times we await on promises (typically returned from async functions), however, it is relatively less well known that await works on arbitrary thenables. By thenables we mean any object with a then function member.

This post covers this usage, and explores some scenarios where it can be interesting.

Using thenables to represent lazy computations

Promises are used to represent output of an operation which has been initiated but may not have yet completed. So by the time we have a promise, the operation it represents has already begun and using await keyword we can (effectively) wait for it to complete.

Thenables can be useful to represent a computation that is yet to begin. For example:

It would be obvious here, that calling fn does not actually initiate an asynchronous operation, but simply returns an object – a thenable.

However, given that await syntax implicitly calls then, we can simply await on the returned thenable exactly in the same way we could have awaited on a promise, but unlike the latter, in this case, calling await will actually initiate the execution of our asynchronous operation.

Libraries like Mongoose (A popular ODM for mongodb) utilize this behavior and implement the thenable protocol in their Query class.

This enables us to await on fluent query builder chains.

Caveats

Even though being a thenable enables Query to “quack” like a promise in this case, an important distinction is that calling then multiple times will trigger multiple calls to the database (unlike a promise which is guaranteed to resolve or reject exactly once).

If this behavior is undesirable, then we can memoize our then function.

Also, note that our fn function above could not be an async function.

An async function is guaranteed to return a promise, and returning a thenable in async function, will cause its then to be invoked.

Stubbing/Spying

Since we know that await implicitly calls promises, we can safely spy on then of a promise in test cases. Or stub async methods to return spied thenables.

Since sinon’s stub/spy functions track invocations, we can later find if makeRequest was invoked as well as whether our (pseudo) promises returned were actually awaited upon.

Dynamic imports and Thenable modules

ES6 modules can export a then method which will be called when these modules are dynamically imported:

Note that static imports are not asynchronous and don’t cause thenables to be executed:

Chaining of thenables

Similar to promises, thenables can be chained:

Usage with Proxies

Dynamic interception of then invocations through proxies works exactly the way we’d expect:

Closure

While implicit resolution of thenables opens up some interesting opportunities, it can also lead to unexpected/surprising behavior if we are not aware of it.