How to convert a callback into async/await

By

Learn how to convert a callback-based function into async/await in JavaScript by wrapping the body in a Promise and calling resolve, for cleaner code.

~~~

To convert a callback-based function into one you can use with async/await, wrap its body in a return new Promise() call, and instead of invoking the callback with the result, call resolve() with it.

Let’s see this on real code. I had some code that used a callback. Without too many implementation details, here’s the gist of it:

const uploadFile = (callback) => {
  //upload the file, then call the callback with the location of the file
  callback(location)
}

uploadFile((location) => {
  // go on
})

See? I call uploadFile and when it finishes doing what it needs to do, it calls the callback function.

But I was using async/await across all my file, so I decided to use async/await here too, instead of using the callback.

Here’s how I did it: I wrapped all the body of the uploadFile function in a return new Promise() call, and when I got the data I wanted to return, I called resolve():

const uploadFile = () => {
  return new Promise((resolve, reject) => {
    //upload the file, then call the callback with the location of the file
    resolve(location)
  })
}

const location = await uploadFile()

The function you pass to new Promise() receives two functions. You call resolve() with the value you want to hand back, and reject() with an error if something goes wrong. When you await the promise, a resolved value becomes the return value, and a rejected error becomes an exception you can catch with try/catch.

Now I could use the location data in the first level, instead of it being wrapped in the callback function.

It helps me keep the code cleaner and reason better about it.

Remember that await only works inside a function marked async (or at the top level of an ES module). In the example below, the route handler is declared async for this reason.

A full example

If you are interested, here’s the full code of the actual function, so you can see this concept in a larger example. This uploads a company logo to S3, then saves its URL on a job posting:

const uploadFile = (fileName, id, callback) => {
  const fileContent = fs.readFileSync(fileName)

  const params = {
    Bucket: process.env.AWS_BUCKET_NAME,
    Key: `file.jpg`,
    Body: fileContent
  }

  s3.upload(params, (err, data) => {
    if (err) {
      throw err
    }
    callback(data.Location)
  })
}

uploadFile(files.logo.path, job.id, async (location) => {
  await prisma.job.update({
    where: { id: job.id },
    data: {
      logo: location
    }
  })
})

Here’s what I transformed it into:

const uploadFile = (fileName, id) => {
  return new Promise((resolve, reject) => {
    const fileContent = fs.readFileSync(fileName)

    const params = {
      Bucket: process.env.AWS_BUCKET_NAME,
      Key: `job-${id}.jpg`,
      Body: fileContent
    }

    s3.upload(params, (err, data) => {
      if (err) {
        reject(err)
        return
      }
      resolve(data.Location)
    })
  })
}

handler.post(async (req, res) => {
  const files = req.files
  const body = req.body

  const job = await prisma.job.create({
    data: {
      ...body,
      created_at: new Date().toISOString()
    }
  })

  const location = await uploadFile(files.logo.path, job.id)

  await prisma.job.update({
    where: { id: job.id },
    data: {
      logo: location
    }
  })

  res.redirect(`/jobs/${job.id}/payment`)
})

One pitfall to watch

Notice the return after reject(err). Calling reject() does not stop the function, the code after it keeps running. A promise can only settle once, so a later resolve() call gets ignored, but any code between the two still executes. Here it would try to read data.Location while data is undefined, and crash. Always return right after rejecting.

One last thing: if the callback follows Node’s (err, data) convention like this one, Node has a built-in shortcut, util.promisify(), that does this wrapping for you. I wrote mine by hand because I also wanted to change what the function receives and returns.

~~~

Related posts about js: