There is no denying that the async....await
pattern is super simple and has simplified asynchronous programming for developers more akin to server-side programming who feel a little insecure and scared without their comfort blanket of a try....catch
block.
Our conscious mind or left-brain operates in what can be thought of as an abstraction of reality. The universe is an infinitesimal series of events happening simultaneously at the same time that our conscious mind cannot grasp, it thinks sequentially or linearly, and we process one thought at a time.
What we are trying to do with async....await
is to ignore reality and have these async operations appear to be happening synchronously. To escape reality in this fashion is all great until it’s not.
Every so often I see a tweet from someone when they realize that async...await
is fundamentally flawed for reasons that this post will explain……if you have not discovered this yourself.
getify on Twitter: “I’m about to declare async..await dead in my JS programming toolbox.Literally every async function I write, I end up wanting a clean way to handle canceling the function call if it’s paused waiting on a promise.This is such a missed design responsibility for JS. / Twitter”
I’m about to declare async..await dead in my JS programming toolbox.Literally every async function I write, I end up wanting a clean way to handle canceling the function call if it’s paused waiting on a promise.This is such a missed design responsibility for JS.
When I see a tweet like this, I feel empathy and sympathy and faith in the knowledge that another member has joined our real-world fight club. I do not need to tell you what the first rule of this club is.
The first time I got hit by this realization was when I was working on a feature that allowed users to upload large video files into Azure blob storage. As these files were large and they had to be split into separate chunks. I was usingasync...await
in a for...of
loop. Then came the requirement that a user would like to cancel the upload halfway through. It was at that moment that this magical almost synchronous looking code block was not fit for purpose.
There is no getting around it, and there is absolutely nothing to support cancellation in async...await
. Below is a simple example of a dependant call chain:
async function updatePersonalCircumstances(token) { const data = await fetchData(); const userData = await updateUserData(data); const userAddress = await updateUserAddress(userData); const financialStatus = await updateFinancialStatus(userAddress); return financialStatus; } const token = {}; const promise = updatePersonalCircumstances(token);
Here we have a classic promise chain with each call awaiting on the last. What if we want to cancel at updateUserAddress
and not call updateFinancialStatus
?
Now we have arrived at the point of the piece, are you sitting comfortably? Then let me spell it out…..
Once you go into an await
call, you never come out unless the underlying promise either resolves or rejects.
The only way that this chain can get cancelled is to wrap every singleasync..await
call like this:
async function updatePersonalCircumstances(token) { let cancelled = false; // we can't reject, since we don't have access to // the returned promise token.cancel = () => { cancelled = true; }; const data = await wrapWithCancel(fetchData)(); const userData = await wrapWithCancel(updateUserData)(data); const userAddress = await wrapWithCancel(updateUserAddress)(userData); const financialStatus = await wrapWithCancel(updateFinancialStatus)(userAddress); // we check after each call to see if something has happend if (cancelled) { throw { reason: 'cancelled' }; } return financialStatus; function wrapWithCancel(fn) { return data => { if (!cancelled) { return fn(data); } } } } const token = {}; const promise = updateUser(token); token.cancel(); // abort!!!
Unfortunately, we need to check at every call to see if there has been a cancellation. We have pushed full responsibility to the user to do the right thing.
When I first encountered this problem, I was working on an angular project which has a dependency of RxJS. RxJS observables have first-class support for cancellation. The problem with rxjs, is the difficulty of getting up to speed with it, it is vast. I have forgotten most of what I have learned about rxjs observables but they were a really good fit for cancelation. If only JavaScript had native support for cancellation? Well, it sort of does.
I have recently discovered effection.js which came into being to cure this problem but has since pushed the boundaries of what is possible with generators.
With generators, you can return immediately or discard the generator if we want to cancel. With async...await
it is effectively a black box with no such convenience.
Below is a better solution to canceling the promise chain:
function runner(fn, ...args) { const gen = fn(...args); let cancelled, cancel; const promise = new Promise((resolve, promiseReject) => { cancel = () => { cancelled = true; reject({ reason: 'cancelled' }); }; let value; onFulfilled(); function onFulfilled(res) { if (!cancelled) { let result; try { result = gen.next(res); } catch (e) { return reject(e); } next(result); return null; } } function onRejected(err) { var result; try { result = gen.throw(err); } catch (e) { return reject(e); } next(result); } function next({ done, value }) { if (done) { return resolve(value); } return value.then(onFulfilled, onRejected); } }); return { promise, cancel }; } function* updatePersonalCircumstances() { const data = yield fetchData(); const userData = yield updateUserData(data); const userAddress = yield updateUserAddress(userData); const financialStatus = yield updateFinancialStatus(userAddress); return financialStatus; } const { promise, cancel } = runner(updatePersonalCircumstances); // cancel baby!!! cancel();
The above code is a basic implementation of a more thorough example I link to at the end of this post. The key is the cancel function:
cancel = () => { cancelled = true; reject({ reason: 'cancelled' }); };
Calling cancel
rejects the promise but the key to making this cancelable is the fact that the generator function is always in play. We could use the generator throw function as an abort signal to indicate a cancellation, or we could even use the generator’s return function to stop executing the promise chain.
The point I am making here is that the generator is always in play throughout the calling sequence and there is no such convenience in async...await
.
I have created this more involved CodeSandbox which wraps this functionality into a React Hook. I have also used xstate to indicate the various state changes in an async request. Using a finite state machine gives the code a better abstraction to cling to and is superior to a pseudo blocking paradigm that has obvious limitations such as the villain of this article, namely async...await
.
I want to thank the frontside people for opening my eyes to the unmined gold that are JavaScript generators. The sky is the limit, and they can be used in any conceivable environment such as build tooling:
import { createConnection, Connection, ConnectionConfig } from 'mysql'; import { spawn, timeout, Operation } from 'effection'; import { main } from '@effection/node'; import { Deferred } from './deferred'; main(function* prepare(): Operation<void> { let connection: Connection = yield function* getConnection(): Operation<Connection> { // asynchronously wait for 10s and then raise an exception. // if a connection is created before the timeout, then this // operation will be cancelled automatically because the enclosing // operation returned. yield spawn(function*(): Operation<void> { yield timeout(10000); throw new Error('timeout out waiting 10s for mysql connection'); }); // Loop "forever" trying to repeatedly create a connection. Of // course it isn't forever, because this loop is racing against // the timeout. while (true) { try { return yield connect({ user: "root", host: "localhost", port: 3306 }); } catch (error) { // if its a socket error or a MysqlError, we want to try again // otherwise, raise the exception if (!error.errno) { throw error; } } } } try { //now we have the connection and can query, migrate, etc... } finally { connection.destroy(); } }); /** * Create a mysql connection as an effection Operation. */ function* connect(config: ConnectionConfig): Operation<Connection> { let { resolve, reject, promise } = Deferred<Connection>(); let connection = createConnection(config); connection.connect((err?: Error) => { if (err) { reject(err); } else { resolve(connection); } }); return yield promise; }
Check out effection to change your perspective.
I think we have settled for convenience over functionality. I still do use async..await
and it is excellent for a one-call scenario, but I, and many others, have discovered it is minimal for more complex real-world situations.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
21 Replies to "async/await is the wrong abstraction"
“there is absolutely nothing to support cancellation in async…await”
Well, of course there isn’t. The Promise itself doesn’t support cancellation in the way that you seem to expect, and async/await is just syntactical sugar on the Promise API.
The code you share doesn’t fix this deficiency. It just adds cancellation support into an arbitrary multi-promise dependency. async/await isn’t “the wrong abstraction”. It just isn’t sufficient for what you’re trying to do. 🤷♀️
Less click-bait titles. More sharing of cool code! 😉
Agreed. This article is like someone complaining that cookies are dangerous because they have a peanut allergy.
First, something isn’t bad just because it doesn’t suit your personal/immediate needs.
Second, you’re blaming a feature for a challenge caused entirely by one of its parts.
Here is an article that covers this topic in a sensible fashion, from which this one seems a lazy ripoff.
https://blog.bloomca.me/2017/12/04/how-to-cancel-your-promise.html
Paul’s point was exactly what the title said. async/await is the wrong abstraction because it leads to incomplete solutions. It punts on one important aspect of async programming and occupies a valuable API surface.
> It just isn’t sufficient for what you’re trying to do. 🤷♀️
I think Paul’s point is not that Promises are a bad abstraction. It’s that `async/await` doesn’t scale. It’s worthing bearing in mind that async/await isn’t just any old abstraction like a `Map`, `Blob` or `ServiceWorker`. It’s literally something that has been baked into the platform at the lowest level: the very grammar by which we express our programs.
Cast in that light, I think his point that there was a missed opportunity to expand flow control at the structural level to accommodate very common and irksome flow control patterns really does stand. Imagine if there had been an implementation of `try/catch` without the concept of `finally`. Would `catch` be implicity bad? Of course not. But it would mean a lot of headaches and workarounds in the cases where it’s needed, and it would me that practitioners would be left with the feeling that the language designers had come close, but fell ultimately fell just shy of the mark.
I agree this post is kind of clickbait to advertise his plugs. Pretty useless info without providing any solution other than some generator library.
Clickbait, biased and dumb content for showing nothing else than another tool. Instead of solving the problem you claim … Sad…
Or use dot net and https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/cancel-an-async-task-or-a-list-of-tasks
the premise here seems to be that having an opposing view is click bait. i have clearly stated the problem i have used a simple example followed by a codesandbox which i wrote but i suspect most did not read that far and highlighted a cutting edge package. click bait? really. so many posts hauling async await as the second coming so i added some balance. dear oh dear. how sad
it is funny that nobody can put forward an argument that the post is wrong and instead just bleat clickbait
You can use an AbortController to signal that the promise should cancel: https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
if you look at the codesandbox at the end of the post, I do just that
Why is it necessary to introduce a new package if you’re going to use AbortController anyway?
I honestly have no idea what the author is talking about. I try/catch await all the time.
While javascript doesn’t have threads, you’re not going to get task cancellation, and that’s something a developer advanced enough to think he has an opinion about this would be expected to understand.
there is no package in that codesandbox. I wrote that without one. Unless you can point one out
Isn’t this exactly what Promise.race is for?
No examples? No other pattern? Wth is daga1 talking about. await is inherently tied to the pattern of the cancellation token, the fact that you were too lazy to pass won down through doesnt make this abstraction useless. To make this serious youd have to inform why cancellation tokens wouldnt be a solution.
Just cause generators are a more elegant solution doesnt make await crap. In a few weeks/months/years youll stumble upon the concept of async generation (see IAsyncEnumerable) and be back to saying await has a place with generators too…not sure when that will make it to ecmascript though
That is why this is click bait
More reason to use . Net core on the server, since it has cancelation tokens for await.
Async/Await is literally syntatic sugar over generators which is the correct abstraction. This article is like complaining that array.push is bad because it doesn’t handle multiple arguments. There is a slightly more complex version of the tool that is perfectly capable of performing the task you’re looking for. The reason Async/Await was made as streamlined as it was is simply because the vast majority of use cases did not require cancellation and therefore made the prospect of juggling another cancellation variable impractical. You solve issues for the masses and provide more powerful tools for the times those mass solutions don’t cover the use-case.
Async/Await wasn’t meant to be the end-all be-all of asynchronous programming in JavaScript. Generators are still valid, as are callbacks. There are more powerful tools designed to support the use cases you are looking for without the streamlined nature of Async/Await. This feature was designed to drastically simplify the majority of web-tasks to do with Asynchronous programming. The vast majority of asynchronous code written for the web is literally do this, then this, then this. Cancellation is only a consideration in a fraction of a fraction of applications and so jamming that into Async/Await didn’t make sense as most developers would never use it and would just discard the cancellation reference as soon as they could. Generators are a lower level and more powerful abstraction specifically there to support more demanding use-cases such as yours. Because truthfully, once you need cancellation, you start wanting progression updates, real-time status capabilities and more because you are no longer firing off an asynchronous pipeline, you are managing an asynchronous process that your consumer is engaging with.
Programmer doesn’t read the requirements properly.
Programmer doesn’t want to use a simple queue.
Programmer blames async, await, his dog 🐕 and his cat🐈.
It’s worth noting that when Apple implemented its own async / await primitives for Swift, they went with something closer to what Paul is advocating rather than JavaScript’s version. This is because automated task cancellation is something you will almost always need to build concurrent systems that are correct, and the lack thereof means the introduction of non-trivial ad-hoc workarounds like abort controllers that have to be passed around explicitly.
Swift, Java, Kotlin, and a growing number of languages are coming to this realization and have provided, or are in the process of providing the primitives to do so. JavaScript *could* have done this, but it would have meant basing async/await not on promises, but on some form of cancellable task.
For those coming to this post just now: the issues identified here are applicable to any language you happen to be working in, and when you encounter them in JavaScript, Paul has pointed out real, practical solutions.
For those who knee-jerked into claims of “clickbait” without doing the intellectual work to actually understand the position before attacking it: history is now beginning the process of not treating your takes very kindly. I hope you can learn that when it comes to a comment section, a single question is worth a thousand opinions.
See https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md for reference