A Visual Guide to Refactoring Callback Functions to Promises & Async/await
Tags: node, beginners, javascript, webdev
Published: 2021-09-06 09:58:26

*This article was originally published at https://maximorlov.com/visual-guide-refactoring-callback-functions/*

Are you constantly struggling to keep your code at least halfway understandable while having deeply nested calls everywhere?

**Callback trees a million deep are distressing.**

Perhaps you're still not comfortable with async/await and you're stuck using promises.

But what if you understood how async/await works, what would you accomplish? A successful job interview, recognition for your skills, or maybe a promotion?

Imagine working with **code that's easy to understand and change**, how would that change how you feel about your work?

By learning the simple approach of identifying and isolating the individual parts involved in asynchronous code flow, you will avoid introducing bugs in the refactoring process.

You'll learn a new skill that will give you the confidence to turn callback hells into async joys.

## A primer on Node.js callback convention
[Callbacks can be either synchronous or asynchronous](https://maximorlov.com/synchronous-vs-asynchronous-callbacks/). When talking about asynchronous callbacks in Node.js, the following two points are true in most cases:

1. The callback function is always the *last* argument passed to an asynchronous function, preceded by other arguments (if any):

```js
// The callback function is the last argument to an asynchronous function
asyncFunction(...params, callback);
```

2. If an asynchronous operation fails, the error object will be the *first* argument passed to the callback function. In case of a success, the error argument will be `null` followed by 0, 1 or more return values:

```js
// An error-first callback function
callback(error, ...results) {
  if (error) {
    // Handle error
    return;
  }

  // Do something with the result...
}
```

This [error-first callback style](https://nodejs.org/en/knowledge/errors/what-are-the-error-conventions/) has become a standard in the Node.js community. It's a familiar pattern that makes working with asynchronous code easier.

## Parts of asynchronous code flow
Asynchronous code can be broken into a few different parts. Identifying and isolating these individual parts before refactoring is *key* to not breaking your code in the process.

The five parts are:
- Function execution (with arguments, if any)
- Error object
- Return value(s)
- Error handling
- Using return value(s)

Throughout this article, we'll use reading the contents of a file in Node.js as an example. We'll start with the callback approach, then refactor that into a promise, and lastly refactor to use async/await.

Here's an exercise for you — before reading on, try to identify and isolate *all* five parts in the following code snippet.

![Using fs.readFile to read a file and passing a callback function with two arguments: error and data. If the error exists it is handled first and otherwise the return value is used.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wf1kc6ritr2lx54j0vz5.png)
<figcaption>Reading a file in Node.js using a callback function</figcaption>

Go ahead, I'll wait.

.
.
.
.
.
.
.
.
.
.

Did you correctly identify all parts involved in asynchronous code flow? Compare your answer with the image below:

![Reading a file in Node.js with fs.readFile and a callback function. Five parts of asynchronous code flow are circled: function execution, error object, return value, error handling and using the return value.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtw0nsmi75rq601bvhwu.png)

<figcaption>Individual parts of asynchronous code flow</figcaption>

> Note: The `return` keyword is an [Return Early Pattern](https://medium.com/swlh/return-early-pattern-3d18a41bba8) to avoid wrapping and indenting the remaining code in an `else` block. Returning nothing is the same as not using the keyword at all — in both cases the function's return value is `undefined`. In the promise & async/await examples further in this article the return keyword is left out, and because that doesn't change the function definition it's safe to exclude it from the error handling part.

## Refactoring callback functions to promises
Once you've identified and isolated the individual parts, you're ready to refactor the callback function to use its promise counterpart.

**While refactoring, it's important to remember to not change anything internal to the individual parts.**

Refactoring a callback function to a promise is done by moving the parts as a *whole* and putting them together in a different way.

The following animation explains this process visually:

<video autoplay loop muted playsinline controls src="https://maximorlov.com/videos/callback-to-promise.mp4" aria-label="Animation starting with reading a file in Node with fs.readFile and a callback function, and ending with its promise counterpart. In the transition all parts of async code flow are circled and shown how they move around.">
</video>

>Note: The second `fs.readFile` is the promise-based equivalent from the `fs/promises` Node.js API namespace. Most libraries provide promise alternatives to callback functions. In case yours doesn't, or if you're depending on custom legacy code, you can [promisify a callback-based asynchronous function](https://maximorlov.com/using-callbacks-with-async-await/).

The parts that are handling the error and using the return value are short one-liners for example purposes. In your situation, they will likely be much bigger, but the principle remains the same — the parts should be moved as a whole unit *without* modifying them or breaking them apart.

A noticeable difference between callback functions and promises is that error handling (failure) is separated from using the return value (success). This visual separation is a better representation of the two diverging code paths and is, therefore, is easier to work with.

## Refactoring promises to async/await
Refactoring callback functions straight to async/await involves multiple steps and will take some practice before you get the hang of it.

It might be easier and less error-prone to add an intermediary step to the refactoring process. First, refactor the callback function to a promise, and *only then* refactor the promise to use async/await.

This is how the transition from a promise to async/await looks visually:

<video autoplay loop muted playsinline controls src="https://maximorlov.com/videos/promise-to-asyncawait.mp4" aria-label="Animation starting with reading a file in Node with fs.readFile .then/.catch, and ending with async/await. In the transition all parts of async code flow are circled and shown how they move around.">
</video>

Notice how much less movement there is compared to the previous animation that went from a callback function to a promise. Because the success and failure parts are separately kept, refactoring a promise to async/await is mostly about changing the syntax.

In case you need to brush up your [understanding of async/await](https://maximorlov.com/understanding-async-await/), go ahead and give the article a read.

## Conclusion
It takes a lot of practice before you're able to effortlessly refactor callback functions into promises & async/await.

By first identifying and isolating the individual parts involved in asynchronous code flow, you're less likely to break your application while refactoring.

Now it's your turn to get rid of nightmare-inducing legacy code and do a long-awaited (pun not intended) cleanup. The codebase will be easier to read, maintain, and most importantly, a joy to work with. ✨

## Turn deeply nested callback trees into easy-to-read asynchronous code
Learn how to turn unmaintainable code into code that is easy to read and change with a **FREE 5-day email course**.

You'll get the **Refactoring Callbacks Guide** that has visual explanations of how to convert nested callbacks to async/await. Using a simple yet effective 5-step approach, you'll gain the confidence to refactor deeply nested callback hells without introducing new bugs.

Moreover, with **30+ real-world exercises** you'll transfer knowledge into a practical skill that will greatly benefit your career.

👉🏼 [**Get Lesson 1 now**](https://maximorlov.com/refactoring-callbacks-course/)