Link copied!
Simplifying Asynchronous JavaScript with async/await and Promise Chains Technical Log

TechiesAIE Journal

Simplifying Asynchronous JavaScript with async/await and Promise Chains

TechiesAIE
TechiesAIE
Lead Developer · TechiesAIE
4 min read 789 words

Based on the sources linked below.

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

JavaScript applications frequently perform operations that don't block the main thread, such as fetching data from a network or reading files. Traditionally, these asynchronous tasks were managed using callback functions, which could lead to complex and hard-to-maintain code known as 'callback hell.' Modern JavaScript addresses this with Promises and the `async/await` syntax, providing more structured ways to handle asynchronous flows.

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Instead of passing callbacks directly into a function, you attach callbacks to the returned Promise object. This approach offers several advantages, including improved readability and a standardized way to handle errors across multiple asynchronous steps.

The Evolution from Callbacks to Promises

Consider a function that asynchronously generates an audio file. With a traditional callback-based API, you might pass separate success and failure callbacks:

function createAudioFileAsync(audioSettings, successCallback, failureCallback);

Calling this function would look like: createAudioFileAsync(audioSettings, successCallback, failureCallback);.

If `createAudioFileAsync()` were rewritten to return a Promise, the callbacks would instead be attached using the `.then()` method: `createAudioFileAsync(audioSettings).then(successCallback, failureCallback);`

Promise Chaining for Sequential Operations

A common requirement is to execute multiple asynchronous operations sequentially, where each operation depends on the success and result of the previous one. Promises simplify this through chaining. The `.then()` method returns a new Promise, representing the completion of both the original operation and its attached callback. This allows you to build a chain where each `.then()` handler processes the result of the previous Promise.

For instance, to perform three sequential operations (`doSomething`, `doSomethingElse`, `doThirdThing`), a Promise chain would look like this:

doSomething() .then((result) => doSomethingElse(result)) .then((newResult) => doThirdThing(newResult)) .then((finalResult) => { console.log(`Got the final result: ${finalResult}`); }) .catch(failureCallback);

It is critical to always return Promises from `.then()` callbacks. If a handler initiates a Promise but does not return it, the subsequent `.then()` in the chain will not wait for that Promise to settle, potentially leading to race conditions or incorrect data due to a "floating" Promise.

Simplified Error Handling

One of the significant advantages of Promises is their centralized error handling. Instead of multiple `failureCallback` functions in nested callbacks, a single `.catch()` method at the end of a Promise chain can handle rejections from any preceding Promise in the chain. This mimics the behavior of `try...catch` blocks in synchronous code, ensuring that all errors, including thrown exceptions and programming errors, are caught.

Introducing async/await

The `async/await` syntax, built on top of Promises, further simplifies asynchronous code by allowing you to write it in a way that looks synchronous. An `async` function implicitly returns a Promise, and the `await` keyword can be used inside an `async` function to pause its execution until a Promise settles (either fulfills or rejects).

The previous Promise chain example can be rewritten using `async/await` for enhanced readability:

async function logIngredients() { const url = await doSomething(); const res = await fetch(url); const data = await res.json(); listOfIngredients.push(data); console.log(listOfIngredients); }

This makes the flow much clearer, as if the operations were executing synchronously. Errors within `async/await` functions can be handled using standard `try...catch` blocks, providing a familiar error management pattern for asynchronous code.

Concurrency with async/await and Promise Combinators

While `await` pauses execution within an `async` function, it does not stop the entire program. Other asynchronous jobs can still run. To manage multiple Promises concurrently, methods like `Promise.all()`, `Promise.allSettled()`, `Promise.any()`, and `Promise.race()` are used. For example, `Promise.all()` takes an array of Promises and returns a single Promise that resolves when all of the input Promises have resolved, or rejects if any single input Promise rejects.

Using `Promise.all()` with `async/await` allows for efficient concurrent execution:

async function concurrentExample() { const [result1, result2] = await Promise.all([ func1(), func2() ]); // Process result1 and result2 }

Practical Uses and Limitations

Promises and `async/await` are fundamental for modern JavaScript development, enabling clean handling of I/O operations, network requests, and other time-consuming tasks without freezing the user interface. They are especially useful in web applications for fetching API data, handling user interactions, and performing background computations.

A key point to remember is that `await` can only be used inside an `async` function. Attempting to use `await` outside of an `async` context in regular JavaScript will result in a `SyntaxError`, though it can be used on its own with JavaScript modules. While `async/await` simplifies code, it's essential to understand that it is syntactic sugar over Promises, inheriting their underlying behavior regarding microtask queues and execution timing.

When comparing to older callback patterns, Promises offer strong semantic guarantees: callbacks registered with `.then()` will never be invoked before the completion of the current JavaScript event loop run, ensuring asynchronous execution and predictable ordering.

Sources