@radekmie
By Radosław Miernik · Published on · Comment on Reddit
The vast majority of the code that most programmers work with is executed synchronously. In this setting, every instruction1 is computed separately, one at a time, in the order defined by the language or the runtime.
However, most programs operate with a database, communicate with external services, or do some sort of background computation. All of these operations have some kind of inherent delay. Classically, the program will pause and wait for the result. Yes, it’ll block the UI as well.
But there’s another way! We could perform these operations asynchronously, i.e., schedule them as well as some code that will be executed once the result is ready. There are many ideas for implementing these APIs, with varying levels of abstractions and performance considerations.
Ideally, we would abstract the waiting away, but we can’t. Or can we?
async
Making a part of the existing codebase async may sound trivial – especially for the stakeholders – but it has immediate and severe consequences. Why? Because every other piece of code using it has to become async as well. If the architecture wasn’t “ready” for it, it will take tens or even hundreds of lines just to propagate this one change.
I think that the virality2 of async
has been the single most critiqued feature in a long time. Especially now, when more and more languages introduced some sort of syntax-level support – JavaScript, Python, Rust…
At the same time, I’d say that introducing the async
keyword is the best thing that has happened to modern JavaScript. Actually, it made it modern, in a way. (See On JavaScript Ecosystem for more “modernizations”.)
Some languages have some sort of an “async escape hatch”. For example, in Java, the Future.get
method will block the execution until the result is ready. For extra convenience, there’s often another option to wait with a timeout.
If you ever implemented such “waiting” in a low-level language, you know that it’s not that simple. Especially, if you don’t want to implement “busy waiting”, i.e., waiting by repeatedly checking the condition. In some situations, it’s fine, but usually, it’s not, as it tends to max up the CPU while doing so.
On the other hand, there’s nothing like that in JavaScript; at least not by default. Meteor, a Node.js framework, relies on the fibers
package to wrap the Promise
s, making it possible to “unwrap” it. Just keep in mind that this package has its downsides, and there’s an ongoing discussion on removing it.
As I already said, being able to write isomorphic (in terms of synchronicity) code is our holy grail. Surprisingly, it’s already possible in Nim! The idea is simple: implement an async function and generate the sync one.
As said in the docs, the multisync
pragma (macro) will do exactly that. When we look into the code, it literally strips down all await
s. If you’d like to see it in practice, check out this Implementing Redis Client tutorial.
Actually, async
is a macro in Nim as well. What it does, is replace all of the await
s with the appropriate iterators; await
is basically a yield
+ read()
. (I had a blast while working with Nim because I always could jump to the code and see how these complex ideas – like parallel
– are implemented.)
Let’s think about how we could achieve the same level of simplicity in JavaScript. Of course, we could do the same transformation as multisync
does, e.g., with a Babel plugin. But to make it work, we have to be able to actually wait.
Let’s take a step back and think about what we actually want. We’d like to have an option to maybe wait for a Promise
to resolve and then continue processing. In other words, we’d like to have something like then
that accepts bare values.
While rethinking the form validation flow in uniforms (#711), I wanted to do precisely that. Previously, even if the entire validation flow was synchronous, the result was a Promise
– just because some parts could be async.
As a result, I created a then
function that receives two arguments. The first one is either a value (of type T
) or a Promise
of it (Promise<T>
). The second one is a function (T -> U
), that maps the first argument. The implementation is trivial. Try it yourself: (x, f) => x instanceof Promise ? x.then(f) : f(x)
.
Once you have it, you can use it the same way as .then()
on the Promise
object. There are two significant differences: no error handling and the potential of unleashing the callback hell (once again). While the former could be solved by adding a try..catch
block and a second function for the error flow, the latter is solvable only with code transformations (again, like a Babel plugin).
Please note that adding the error handling to it would bring it much closer to the Result
type in Rust and other languages. Whether or not the Promise
s should “carry” their error types is another topic3.
await
in JavaScriptAll of this fuss, and we finally ended up with a Promise
-like then
function. Was it even worth it? Why won’t we stick to the await
literally everywhere? Well, there’s a problem. A subtle but important one.
Because of the way how ECMAScript standard defined await
, it adds some slight overhead delay. It’s barely noticeable but quickly accumulates – even in real-life scenarios. It got significantly better with this change, foreshadowed in this excellent writeup and released in V8 v7.2.
However, it’s still not ideal. For example, for await..of
is significantly slower than for..of
for synchronous iterators (nodejs/node#31979; especially this). That’s again a problem that may be solved by changing the standard, but such changes are relatively rare.
And last but not least, using await
always creates new Promise
objects. No, I won’t rant about the memory usage here, as the modern engines excel at handling that. The problem is much simpler: sometimes you’d like to branch off a Promise
just to handle the error.
The idea of multisynchronicity is fairly simple yet basically non-existent in mainstream programming languages. But maybe it’s going to be the next big thing – just like async
. I’m not so sure about it, but it’d be awesome.
As for now, let’s stick to async
. It really makes the code easier to comprehend and maintain. The performance overhead is there, sure, but so are the benefits. It’s up to you to decide, but for me, it’s a no-brainer. It’s just worth it.
Write idiomatic code. Performance will come next. It always does.
An instruction doesn’t necessarily have to be an assembler one. In this context, it can be a statement, expression, or any kind of computation in your language.
If you’d like a more creative analogy, try coloring your functions red and blue.
In short: they won’t. If you’d like them to, just use a handmade Result
type. Or use any of the hundreds of TypeScript packages that do it.