Internals of async / await in JavaScript
akashhamirwasia.com
akashhamirwasia.com
For instance look at the JS output of async/await Typescript when a really old JS version is used that didn't support async/await yet.
It's switch-case all the way down:
https://www.typescriptlang.org/play?target=1#code/FAMwrgdgxg...
I think what another comment mentioned is more accurate:
> Asnyc/await is just syntactic sugar for promises.
> Promises are just wrappers around callbacks.
> Callbacks are used to hook functions into an event loop. The dispatching/scheduling is handled there.
> So really when you use async/await, you’re just writing pseudo-sequential code that gets turned into callback code.
Small nitpick:
I wouldn't call it "pseudosequential", personally. By that logic, C is pseudosequential, because when you call a system call, your process state gets saved in the scheduler, control gets passed to the kernel, then when kernel finishes, your process state is resumed. Not even getting started on branch prediction and similar CPU optimizations.
What I mean is, as long as you have a kernel and scheduling, there's no such thing as a real (userspace) sequential program. We just call them sequential because their side effects correspond to a sequential model of execution. So if the side effects of an async/await program corresponds to that of a sequential model, it's a sequential program.
But I get the value of using that word for an explanation of the implementation.
> Generators are a special type of function that can return multiple pieces of data during its execution. Traditional functions can return multiple data by using structures like Arrays and Objects, but Generators return data whenever the caller asks for it, and they pause execution until they are asked to continue to generate and return more data.
Applications of generators? I have only used Redux-Saga[1]. Can't even think of other libraries that use them, but would be interested in learning.
Some patterns become easier to write with the generator syntax, but it's not really adding expressive power. (Unlike futures for instance)
Generators allow you to write a state machine as a procedural function whose local variables are automatically suspended when you yield and restored when the caller returns control to the generator. They compile this down to, as you said, an object with a call method. But that transformation is as nontrivial as any compilation/transpilation project.
The most common application is iteration (lazy producers or transformers), but they're also used for their ability to suspend and resume function execution (interruptible functions) e.g.
- pytest fixtures use generators to setup, allow the test to run (yielding a value or not), and teardown
- contextlib.contextmanager uses generators for similar purposes of entering a context, allowing the wrapped code to run, and then exiting the context
They can also act as cheap state managers, especially with the ability to inject data back into the generator, so they can be used to implement a multi-step function where the user needs multiple points of interaction, without needing a bunch of callbacks, or complicated state management (especially without the benefit of sum types or static state machines)
The advantage of generators over arrays is that you don't have to allocate a large array, so you can handle large numbers of elements while keeping a lower memory footprint.
They can also hide batching/chunking logic. You could have code that repeatedly downloads of separate JSON files with 200, or 500, or 5000 elements at a time, then return a generator that goes through each element at a time. The consuming code will be none the wiser.
Before async/await, generators were the only way to express async flow control in a synchronous way. Libraries like “co” were really popular at the time.
There are some really downside to async/await which is why redux-saga continued to use generators. Even in 2023, me and a couple others are experimenting with deliminited continuations using generators as the foundation.
A couple of libraries we have built:
- https://github.com/thefrontside/continuation
- https://github.com/thefrontside/effection/tree/v3
- https://github.com/neurosnap/starfx
The last one intends to replace redux-saga using DCs.
Here’s a presentation I gave recently talking about DCs in typescript: https://youtu.be/uRbqLGj_6mI?si=XI0JNMKMoO2VHMvM
You can implement basic operators like map, filter, take, etc. over generators to create pipelines of operations. Very neat abstraction to work with, but like rx, can quickly get hard to reason about.
Recently I wrote some tooling to read, do some operations, and write hundreds of thousands of files locally. Using generators solved having to think about not loading too much stuff into memory since it only yields files when consumed. Also allows you simply implement stuff like batching, like running X requests to a server at a time, and only starting the next batch once the first one is done.
Glossing over this fact leads to a flawed understanding, not a deeper one.
The generator function keeps track of the current page.
This allows you to use a `for await` loop which is quite concise.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
You can do it without generator functions, but generator functions allows the caller to not have to know about the underlying pagination API.
A generator is nice here because it makes it easy for the code that converts the JSON for transmission to wait until the OS has transmitted the previous chunks -- which helps avoid catastrophic back pressure and runaway memory use.
For example, do you have a stream of chunks, and want to turn it into a stream of lines? The implementation of this with generators is super clean and readable, because it’s like you’re just iterating over a list.
It included try/catch support and fancy stuff, like loops. The source may be interesting for anyone interested in compilers: https://github.com/rzimmerman/kal/blob/master/source/generat...
The Kal compiler is written in Kal, but it's supposed to be easy to read. Surprisingly the browser demo still works: http://rzimmerman.github.io/kal/demo.html
I have been looking forward to a type-safe coffee-script. Civet.dev comes close but goes into some pretty esoteric directions.
Practically speaking it’s easier to grasp. The article in question makes a good point of playing around with the concepts to dispel the magic.
Asnyc/await is just syntactic sugar for promises.
Promises are just wrappers around callbacks.
Callbacks are used to hook functions into an event loop. The dispatching/scheduling is handled there.
So really when you use async/await, you’re just writing pseudo-sequential code that gets turned into callback code.
This is also why a function can only await if it’s marked as async. It’s callbacks, function coloring leaking out so to speak.
This part never really made it into my understanding of the concept. For practical purposes I've been completely fine with not thinking about it at all. My function is called at some point, that's all what I find really matters. Thinking about the event loop opens up a can of worms I find more confusing than useful.
Probably for daily usage this might be true, but there are definitely cases when you're using JavaScript and not understanding the event loop makes it hard to understand why things are happening. Example:
setTimeout(() => console.log('hello'), 0)
console.log('world')
You'd normally expect this to print "hello" and then "world" since the "sleep time" for the setTimeout is set to 0, but thanks to the event loop, the result will be "world" and "hello". Very simple example, but in real life code bases and beginner JavaScript developers, in can be confusing at times.I've seen smart and capable programmers who are not used to UI programming and async in general who struggle with callbacks and IoC initially. You can draw a simple picture on a whiteboard to explain the overall concept to give them something to grasp on.
I haven't used that much JS recently, but my guess is that "for await of" will make generators more widely known and used.
Thank you to everyone who read my article! Absolutely love some of the discussions going on here. My goal with the article was to peel away one layer of this abstraction and encourage curiosity in engineers who usually don't think about lower level stuff.
Here is the implementation I typically use for doing just that:
async function executeQueue(promises, concurrentLimit) {
let index = 0;
let activePromises = [];
async function manageQueue() {
if (index >= promises.length && activePromises.length === 0) return;
while (activePromises.length < concurrentLimit && index < promises.length) {
const promise = promises[index]();
activePromises.push(promise);
index++;
promise.then(() => {
activePromises = activePromises.filter(p => p !== promise);
manageQueue();
});
}
}
await manageQueue();
await Promise.all(activePromises);
}
Used like this: function r(i, resolve) {
console.log(`Task ${i}`)
resolve(`Task ${resolve}`)
}
// Example usage
const promises = [
() => new Promise(resolve => setTimeout(() => r("Task 1", resolve), 2000)),
() => new Promise(resolve => setTimeout(() => r("Task 2", resolve), 1000)),
() => new Promise(resolve => setTimeout(() => r("Task 3", resolve), 3000)),
() => new Promise(resolve => setTimeout(() => r("Task 4", resolve), 500)),
() => new Promise(resolve => setTimeout(() => r("Task 5", resolve), 1500))
];
executeQueue(promises, 4).then(() => {
console.log("All tasks completed!");
});
Would print: "Task Task 4"
"Task Task 2"
"Task Task 1"
"Task Task 5"
"Task Task 3"
"All tasks completed!"If you don't want to continue with something inside a Promise, just pass in something that could be changed from `true` to `false` and it cancels itself based on that.
Regarding other cases, there are already ways of handling those. I don't think just because many ask about something, doesn't mean it's not currently working.
controller = new AbortController();
const signal = controller.signal;
fetch("http://example.com", { signal: signal })
// abort request
controller.abort();
It basically couldn't be simpler? What sort of API would you like to be able to cancel requests?