Unraveling callbacks with async functions
Tags: javascript, tips, webdev, readability
Published: 2019-07-10 13:48:13

---
title: Unraveling callbacks with async functions
published: true
description: Using async functions we can make our code easier to read and maintain, on this article I'll show you how I refactored real world code.
tags: javascript, tips, webdev, readability
canonical_url: https://engineering.spideroak.com/unraveling-callbacks-with-async-functions/
---


## Requirements

I assume you are familiar with Javascript and these concepts:
* [Arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
* [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises)
* [Async functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)


## Example and problems

This is a real life example of how a function that moves a file looked. This was part of one of our mobile apps.

The code is not really complex, but it was hard to read at a glance; it felt bad.
So I tried to refactor it a little to see if I could improve its readability.


```javascript
import path from 'path';

/**
 * Moves a file from one directory to another.
 *
 * @param { String } basePath: the base path for both relativeSourcePath
 *                             and relativeDestinationPath.
 * @param { String } relativeSourcePath: the relative path of the file.
 * @param { String } relativeDestinationPath: the relative new path of the file.
 *
 * @return { Promise } resolves with no value if the file is
 *                     successfully moved.
 */
function move(basePath, relativeSourcePath, relativeDestinationPath) {
  return new Promise((resolve, reject) => {
    const destinationPath = path.dirname(relativeDestinationPath);
    const filename = path.basename(relativeDestinationPath);

    ensureDirectory(basePath, destinationPath).then(() => {
      window.resolveLocalFileSystemURL(basePath, baseDirEntry => {
        baseDirEntry.getFile(relativeSourcePath, {}, sourceFileEntry => {
          baseDirEntry.getDirectory(destinationPath, {}, destDirEntry => {
            sourceFileEntry.moveTo(destDirEntry, filename, resolve, reject);
          }, error => {
            console.error('[move] Error getting destination directory', error);
            reject(error);
          });
        }, error => {
          console.error('[move] Error getting source file', error);
          reject(error);
        });
      });
    }).catch(error => reject(error));
  });
}
```

The problem here is mainly that we have a deeply nested code, which makes it harder to reason about, maintain and debug.


## The strategy

To understand what was going on, what I tried to do is to visually isolate callbacks, identify relevant data we were extracting from each call, and where we were using it.

After that, I wrapped the functions on `await` and `Promise` to simulate a regular function that returns a value.

Let's see how we go from a callback based function to an async function.

```javascript
// you call this `doStuff` function to do something and you get `data` if it
// succeeds or an `error` if it fails.
doStuff(param1, param2,
    data => {
      /* do something with the data */
    },
    error => {
      /* problem with doStuff */
    }
  );

// We can extract our functions to handle success and failure like so:
const onSuccess = data => { /* do something with the data */ }
const onFailure = error => { /* problem with doStuff */ }

doStuff(param1, param2, onSuccess, onFailure);
```

Now, let's use a `Promise` to wrap our call and `await` for its result.

```javascript
try {
  const data = await new Promise((resolve, reject) => {
    const onSuccess = data => resolve(data);
    const onFailure = error => reject(error);
    doStuff(param1, param2, onSuccess, onFailure);

    // we don't really need a separate definition for the functions
    // we can inline them like so:
    doStuff(param1, param2, data => resolve(data), error => reject(error));
  });

  /* do something with the data */
} catch(error) {
  /* problem with doStuff */
}
```

Or alternatively, as a one liner.

```javascript
try {
  const data = await new Promise((resolve, reject) => doStuff(param1, param2, data => resolve(data), error => reject(error)));
  /* do something with the data */
} catch(error) {
  /* problem with doStuff */
}
```

The success/failure handlers are a bit redundant, so let's simplify that.

```javascript
try {
  const data = await new Promise((resolve, reject) => doStuff(param1, param2, resolve, reject));
  /* do something with the data */
} catch(error) {
  /* problem with doStuff */
}
```

And there we go, our final shape. It doesn't look like much of a change, but this allows us to have a more shallow code instead of a really nested set of callbacks.


## Final result

Here's how our function looks after refactoring it using the above strategy.

```javascript
import path from 'path';

/**
 * Moves a file from one directory to another.
 *
 * @param { String } basePath: the base path for both relativeSourcePath
 *                             and relativeDestinationPath.
 * @param { String } relativeSourcePath: the relative path of the file.
 * @param { String } relativeDestinationPath: the relative new path of the file.
 *
 * @throws { Error } if there was a problem moving the file.
 */
async function move(basePath, relativeSourcePath, relativeDestinationPath) {
  const destinationPath = path.dirname(relativeDestinationPath);
  const filename = path.basename(relativeDestinationPath);

  try {
    await ensureDirectory(basePath, destinationPath)

    const baseDirEntry = await new Promise(resolve =>
      window.resolveLocalFileSystemURL(basePath, resolve)
    );

    const sourceFileEntry = await new Promise((resolve, reject) =>
      baseDirEntry.getFile(relativeSourcePath, {}, resolve, reject)
    );

    const destDirEntry = await new Promise((resolve, reject) =>
      baseDirEntry.getDirectory(destinationPath, {}, resolve, reject)
    );

    await new Promise((resolve, reject) =>
      sourceFileEntry.moveTo(destDirEntry, filename, resolve, reject)
    );
  } catch (error) {
    // here you can do something about this problem
    console.error('There was a problem moving the file.', error);
    throw error;
  }
}
```

For this particular case, it didn't make much sense to log two different errors, so I wrapped all the calls in a `try`/`catch` and just logged the problem there.

Your use case may vary and you may want to have more than one handling block or none at all and document that your function may `throw` different errors. This is useful if you don't want to perform a specific action on this function when an error occurs, and leave it to the caller.


## Last words

With just a little of work, our code is now easier to read and maintain.

This problem is quite common and it's usually called "callback hell", as you can see here: http://callbackhell.com/

I hope this article gives you some ideas on how to make your life easier.


## Disclaimer

I wrote this article for the SpiderOak engineering blog and it was published on Jul 10, 2019.
https://engineering.spideroak.com/unraveling-callbacks-with-async-functions/

The original post is licensed as: [Creative Commons BY-NC-ND](https://creativecommons.org/licenses/by-nc-nd/4.0/)