Link copied!
Simplifying Asynchronous JavaScript with async/await for Cleaner Code Technical Log

TechiesAIE Journal

Simplifying Asynchronous JavaScript with async/await for Cleaner Code

TechiesAIE
TechiesAIE
Lead Developer · TechiesAIE
4 min read 776 words

Based on the sources linked below.

Cover image: Martin Vorel · CC BY-SA 4.0 · License · Image source

JavaScript's async/await syntax offers a more intuitive way to write asynchronous code, making it appear and behave much like synchronous code. This approach simplifies the handling of Promises, which are objects representing the eventual completion or failure of an asynchronous operation.

Before async/await, handling multiple asynchronous operations sequentially often led to 'callback hell' or deeply nested Promise chains, which could be challenging to read and maintain. Async/await builds upon Promises, providing syntactic sugar that enhances readability and simplifies error handling through familiar try/catch blocks.

How async/await Simplifies Promise Chains

When you call an async function, it always returns a new Promise. The `await` keyword, permitted only inside async functions, pauses the execution of the async function until the Promise it's waiting for is settled (either fulfilled or rejected). The resolved value of the Promise then becomes the return value of the `await` expression.

Consider a scenario where you need to perform several asynchronous operations in sequence, with each subsequent operation depending on the result of the previous one. With traditional Promises, this involves chaining multiple `.then()` calls. For example, downloading data, then processing it, and finally logging the result.

Using `async/await`, this sequence becomes significantly flatter and easier to follow. Instead of chaining `.then()` callbacks, you can declare an `async` function and use `await` before each Promise-returning call, effectively pausing execution until that specific Promise resolves before moving to the next line of code.

Practical Example: Sequential Data Processing

Imagine a function `getProcessedData(url)` that first downloads data from a URL, then possibly downloads fallback data if the first attempt fails, and finally processes the retrieved data. Here's how it could look using `async/await`:

async function getProcessedData(url) { let v; try { v = await downloadData(url); } catch (e) { v = await downloadFallbackData(url); } return processDataInWorker(v); }

In this example, `downloadData(url)` and `downloadFallbackData(url)` are assumed to return Promises. The `await` keyword ensures that `v` is assigned the resolved value of the Promise before the function proceeds. If `downloadData(url)` rejects, the `catch` block is executed, and `await downloadFallbackData(url)` is called. This structure directly mirrors synchronous `try...catch` logic, improving clarity. Alternatively, you can chain the `.catch()` directly onto the awaited Promise: `const v = await downloadData(url).catch((e) => downloadFallbackData(url));`

Error Handling with async/await

One of the key benefits of `async/await` is simplified error handling. Unlike chaining `.catch()` at the end of a Promise chain, `async/await` allows you to use standard `try...catch` blocks. This means any error (including thrown exceptions or rejected Promises) within the `try` block will be caught by the `catch` block, just like in synchronous code.

This symmetry with synchronous error handling makes it much easier to reason about potential failures in asynchronous operations, preventing issues like unhandled promise rejections that might otherwise require global event listeners.

Concurrency and async/await

While `await` makes asynchronous operations appear synchronous, it's crucial to understand how it affects concurrency. An `await` expression only suspends the execution of the async function it's inside, not the entire program. Other asynchronous jobs can still run while an `await` is pending.

For operations that do not depend on each other and can run in parallel, using `await` sequentially would lead to unnecessary delays. For example, if you `await` two independent Promises one after another, the second `await` will only start after the first Promise resolves. To run multiple Promises concurrently and wait for all of them to complete, you should use `Promise.all()` (or `Promise.allSettled()` for scenarios where individual rejections should not fail the entire set), and then `await` the result of `Promise.all()`.

Example of concurrent execution:

async function concurrentOperations() { const results = await Promise.all([ resolveAfter2Seconds(), resolveAfter1Second(), ]); console.log(results[0]); console.log(results[1]); }

In `concurrentOperations()`, `resolveAfter2Seconds()` and `resolveAfter1Second()` are initiated simultaneously. `await Promise.all()` then waits for both to settle before proceeding, ensuring the overall operation completes in the time of the longest Promise, rather than the sum of their individual durations.

Limitations and Considerations

While `async/await` significantly improves readability, it's possible to forget the `await` keyword, which can lead to unexpected behavior where a Promise is used as a value. Additionally, `await` is only valid inside `async` functions (or at the top level in JavaScript modules), meaning you cannot use it in regular, non-async function bodies without triggering a `SyntaxError`.

It's also important to remember that `async/await` does not inherently change the underlying asynchronous nature of JavaScript. It is a syntactic abstraction built on top of Promises, which are processed on the microtask queue after the current execution stack is empty, but before control is returned to the event loop.

Sources