Async by default: JavaScript++
Editor’s Note: I’m not a language designer, nor a compiler writer. I’d love to get feedback from others on why this idea would/wouldn’t work.
Originally appeared on my Medium blog.
Promises are pure genius, and were a significant addition to the JavaScript language in ES2015, built on the shoulders of giants. Now I’m convinced they’re all that developers need to handle any asynchronous situation in their code. (Move along, RxJS.)
But it’s also obvious to me that it was added on later, with backwards compatibility in mind. Which is great, but it also makes me wonder… what if JavaScript was async by default instead? I call this idea JavaScript++.
Let me back up with a basic Promise/await concept that may be new to you — what I call “auto-boxing”:
let a;
a = 5; // 5
a = Promise.resolve(5); // a is a Promise(5)
a = await Promise.resolve(5); // 5
a = await 5; // auto-boxed as a Promise(5), then unwrapped by await
All of these statements are functionally equivalent, even the last one; it’s true! Let’s expand this concept a little bit more by looking at functions:
let syncFunc = () => 5; // returns 5
let asyncFunc = async () => 5; // returns a Promise(5)
let a;
a = syncFunc(); // 5
a = asyncFunc(); // a is a Promise(5)
a = await asyncFunc(); // 5
a = await syncFunc(); // auto-boxed as a Promise(5), then unwrapped
Isn’t this cool? What is means is that “async” is backwards compatible — you don’t have to use it on a Promise! Which begs the question… what if the language could auto-box/unbox everywhere by default? Let’s change our syntax slightly into psuedo-code where “::” signifies a Promise auto-box preference:
let syncFunc = () => 5;
let asyncFunc = () ::=> 5; // returns a Promise(5)
let a;
a = 5;
a = Promise.resolve(5); // a is a Promise(5)
a =:: Promise.resolve(5);
a =:: 5;
a = syncFunc();
a = asyncFunc(); // a is a Promise(5)
a =:: asyncFunc();
a =:: syncFunc(); // auto-boxed as a Promise(5), then unwrapped
The reason we must distinguish between “=” and “=::” is because sometimes we want to allow async work (such as an API call) to run “in the background” while other work is done immediately; we don’t want to block/await immediately. (This is similar to multi-threading concepts where a background thread isn’t “joined”/awaited until absolutely necessary.)
But, do we really need to distinguish between a “() =>” and a “() ::=>” function at all? Why not always return a Promise, and let the caller can decide whether to await “::” or not?
let func = () => 5; // auto-boxed as a Promise
let func = () ::=> 5; // which syntax do you prefer?
let a;
a =:: func();
a = func(); // a is a Promise(5)
Another benefit of ALL functions returning a Promise is that you never have to decide whether a function is “async” or not. This is often a cause for refactoring, when suddenly a function depends on another async function; it must either become async itself, or awkwardly leverage the Promise’s “.then(…)” API. This problem goes away if EVERY function is async!
Finally, to truely make async/Promises default (because we assume maybe that the majority of Promises are immediately awaited ?), we simply swap the “::” syntax to mean “do NOT await”:
let func = () => 5; // auto-boxed as a Promise(5)
let a;
a = func(); // Promise(5) is unwrapped as a 5
a =:: func(); // a is a Promise(5)
// ... do more work ...
a = a // Promise(5) is unwrapped as a 5
a = a // 5 is auto-boxed into a Promise(5), then unwrapped as a 5
Of course, consideration for other operators would also be needed, for example:
let func = () => 5; // auto-boxed as a Promise (5)
let a;
a = func() + func() // plus unwraps all promises
a = func() + 5 // OK
a = func() +:: func() // invalid - cannot add a Promise
a = 5 +:: func() // invalid - cannot add a Promise
a = func() +:: 5 // invalid - cannot add a Promise
I’ll leave it here as an excercise for the reader. 🤔
Admittedly, this JavaScript++ would definitely break backwards compatibilty, and is therefore probably more of a mental excercise than anything else. But… maybe it’s inspired someone smarter than me to figure out the next great leap in JavaScript async handling. Who knows?
“Async by default” — Would it work? Let me know what you think in the comments below.