DEV Community

Cover image for A Visual Guide to Refactoring Callback Functions to Promises & Async/await
Maxim Orlov
Maxim Orlov

Posted on • Updated on • Originally published at maximorlov.com

A Visual Guide to Refactoring Callback Functions to Promises & Async/await

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. 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):
// The callback function is the last argument to an asynchronous function
asyncFunction(...params, callback);
Enter fullscreen mode Exit fullscreen mode
  1. 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:
// An error-first callback function
callback(error, ...results) {
  if (error) {
    // Handle error
    return;
  }

  // Do something with the result...
}
Enter fullscreen mode Exit fullscreen mode

This error-first callback style 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.

Reading a file in Node.js using a callback function

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.

Individual parts of asynchronous code flow

Note: The return keyword is an Return Early Pattern 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:

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.

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:

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, 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

Top comments (0)