August 13th, 2024

I avoid async/await in JavaScript

Cory argues that async/await complicates JavaScript code, obscures optimization opportunities, increases cognitive load in error handling, and suggests promises are cleaner and more manageable for asynchronous operations.

Read original articleLink Icon
I avoid async/await in JavaScript

Cory argues against the use of async/await in JavaScript, claiming it complicates code rather than simplifying it. He believes that while async/await is designed to make asynchronous code easier to read by mimicking synchronous code, it ultimately misleads developers into thinking they are working with synchronous logic. This can obscure opportunities for optimization, such as parallelizing operations that could be executed simultaneously. Cory also critiques the error handling in async/await, noting that it requires a mental shift to manage both return values and exceptions, which can increase cognitive load. He finds the promise-based approach to be more straightforward, as it allows for cleaner error handling without the need for try/catch blocks. Additionally, he challenges the notion that async/await prevents "callback hell," suggesting that well-structured promise chains can avoid deep nesting and maintain readability. Overall, Cory advocates for using promises over async/await, arguing that promises are more versatile and easier to manage in various scenarios.

- Cory believes async/await complicates code by promoting a misleading synchronous mental model for asynchronous operations.

- He argues that async/await can obscure optimization opportunities, such as parallelizing independent operations.

- Error handling in async/await increases cognitive load due to the need to manage both return values and exceptions.

- Cory finds promise-based code to be cleaner and more readable, especially in error handling.

- He challenges the idea that async/await prevents callback hell, asserting that well-structured promise chains can maintain clarity.

Related

Optimizing JavaScript for Fun and for Profit

Optimizing JavaScript for Fun and for Profit

Optimizing JavaScript code for performance involves benchmarking, avoiding unnecessary work, string comparisons, and diverse object shapes. JavaScript engines optimize based on object shapes, impacting array/object methods and indirection. Creating objects with the same shape improves optimization, cautioning against slower functional programming methods. Costs of indirection like proxy objects and function calls affect performance. Code examples and benchmarks demonstrate optimization variances.

Synchronous Core, Asynchronous Shell

Synchronous Core, Asynchronous Shell

A software architecture concept, "Synchronous Core, Asynchronous Shell," combines functional and imperative programming for clarity and testing. Rust faces challenges integrating synchronous and asynchronous parts, prompting suggestions for a similar approach.

Synchronous Core, Asynchronous Shell

Synchronous Core, Asynchronous Shell

Gary Bernhardt proposed a Synchronous Core, Asynchronous Shell model in software architecture, blending functional and imperative programming. Rust faces challenges integrating sync and async functions, leading to a trend of adopting this model for clarity and control.

JavaScript Promises from the Ground Up

JavaScript Promises from the Ground Up

This tutorial explores JavaScript Promises, emphasizing their role in managing asynchronous code. It covers Promise basics, states, handling methods, event loop, practical usage with fetch(), and the shift towards Promise-based operations for enhanced code quality.

Beyond Clean Code

Beyond Clean Code

The article explores software optimization and "clean code," emphasizing readability versus performance. It critiques the belief that clean code equals bad code, highlighting the balance needed in software development.

Link Icon 11 comments
By @mrozbarry - 6 months
My biggest gripe with async await is when it's used by people that don't understand promises. They interpret await as "let promise run" and not "this blocks the flow." I had someone show me code where they wanted to preload a bunch of images, and effectively had a for loop and did an await on each image in it, effectively running it as slow as possible. I showed him `await Promise.all(imageLoadingPromises)`, and he was confused why we needed `Promise.all` here.

I also don't like how async/await ends up taking over a whole codebase. Often making one function async will cause you to update other functions to be async so you can do proper awaiting. We literally exchanged callback hell to async hell, and said it was better, but I'm not strictly convinced of this.

By @ilaksh - 6 months
It's interesting to me how many people can look at the exact same thing and have completely different experiences and takeaways.

I suggest that this is largely determined by things like the person's previous experiences, maybe even their group or personal identity, etc. and various factors that are not necessarily objective.

For me, I strongly feel that using async/await consistently instead of a promise chain style can clean up a codebase quite a lot.

I also feel that whitespace significant syntaxes are (to me) obviously cleaner. So for a year or two at least I used things like CoffeeScript or even ToffeeScript.

But I got tired of feeling like I was swimming upstream.

I also did a solo project with significant usage of LiveScript a few years ago. Which I think is a very nice language.

By @dave4420 - 6 months
I wish we had syntax like

    concurrently {
        const user = await getUser(userId);
        const session = await getSession(sessionId);
    }
meaning that

- the getUser and getSession calls occur concurrently (it desugars to Promise.all)

- user and session are available for use after the block

- user and session appear directly next to the calls that give them their values, unlike with Promise.all

By @spankalee - 6 months
Wait, what?

The author sees this code and realizes it's unnecessarily serial:

    save('userData', userData)
      .then(() => save('session', sessionPrefences))
      .then(() => ({ userData, sessionPrefences })
But doesn't see that the async/await version is serial, even though it's arguably even _more_ obvious?

    await save('userData', userData);
    await save('session', sessionPrefences);
    return { userData, sessionPrefences }
The transform to being parallel is pretty easy to read too:

    await Promise.all([
      save('userData', userData),
      save('session', sessionPrefences)
    ]);
    return { userData, sessionPrefences }
On top of that, the try/catch section is out of date. try/catch does not deoptimize the block in modern engines, and async/await absolutely integrates better with try/catch for much more bulletproof error handling.
By @pkilgore - 6 months
Syntax ain't going to fix people who do not understand their tools.
By @kmoser - 6 months
> Now, there are some schools of programming that lean heavily into try/catches. Me, I find them mentally taxing. Whenever there is a try/catch, we now have to worry not only about what the function returns, but what it throws. We not only have branching logic, which increases complexity but also have to worry about dealing with two different paradigms. A function may return a value, or it may throw. Throwing bubbles if it is not caught. Returns do not. So both have to be dealt with for every function. It’s exhausting.

I tend to agree with this sentiment. I wonder whether it would have been helpful, perhaps in an alternate universe, for all functions to return a structure containing two things: the actual intended return value, and an error object (perhaps NULL if no error, to reduce overhead).

So something like `return $foo;` would imply no error but `return NULL, Error('bar')` would imply an error. Then, we could use `return` at any point to indicate success or an error depending on the case.

The only thing remaining to mimic the behavior of `try...catch` would be to make closures which could be `return`ed from, similar in concept to `throw` in that they would exit the block except that because it's a `return` it would work like any other `return` to exit the block.

Am I crazy, or would this (at least in theory) simplify the return/try/catch issues the author is talking about?

By @odyssey7 - 6 months
> It’s just a tiny hint to get you thinking about what functionally style JavaScript could look like if we wanted.

Yep, JavaScript is excellent as a multi-paradigm language. I pretty much use it as a functional one. I hear the JavaScript edition of SICP [1] works better than the Python one did.

[1] https://mitpressbookstore.mit.edu/book/9780262543231

By @h_tbob - 6 months
Ok that was the weirdest thing I’ve ever read/skimmed.

I don’t know how in the world this guy thinks that async is hard to understand…

I mean I think it’s pretty obvious it’s an async call!

But I didn’t read the whole article!