Using promises for NodeJS and AWS SDK methods

Promises are used every day in NodeJS, because they are much easier to work with than traditional callbacks. Core module and AWS SD asynchronous methods can be converted to promises using internal and external promisify functions.

Let’s first see the NodeJS modules, and then move on to some examples in AWS.

1. NodeJS core module methods

Core module methods come in two versions: synchronous and asynchronous. Some methods (for example, in the fs module) have both versions; others are either synchronous or asynchronous.

Asynchronous methods accept a callback function, which is called after the result (or an error) from running the method is available.

I created a simple hello.txt file in the project folder which has the following content:

This content will be read throughout the post.

This file will be abused in this section.

1.1. The callback-way

Consider the readFile method in the fs module, which reads the content of a file from the file system:

// index.js
const fs = require('fs')
const path = require('path')

fs.readFile(path.join(__dirname, 'hello.text'), (err, data) => {
  if (err) throw err
  console.log(data.toString())
})

When index.js is run and if everything goes well (and why not), the This content will be read throughout the post. text should be displayed in the console.

But callbacks are old and cumbersome, and it’s 2019, so let’s use promises instead.

1.2. Use util.promisify

Node has a cool module called promisify, which transforms the asynchronous, callback-heavy methods to nicer, Promise-based ones.

We need to require the method first, and promisify readFile:

const { promisify } = require('util')

const readFile = promisify(fs.readFile)

We can then work with the Promise:

readFile(path.join(__dirname, 'hello.txt'))
  .then((data) => {
    console.log(data.toString())
  }).catch((e) => {
    console.log(e)
  })

The result should be the same.

Alternatively (and probably as a best practice), we can use an async function:

const main = async () => {
  try {
    const file = path.join(__dirname, 'hello.txt')
    const data = await readFile(file)
    console.log(data.toString())
  } catch (e) {
    console.log(e)
  }
}

main()

1.3. Use promises

From NodeJS version 10, the fs module comes with the promises method, which makes the use of util.promisify unnecessary. The method is stable in version 12:

const { promises: fsPromises } = require('fs')

const main = async () => {
  try {
    const file = path.join(__dirname, 'hello.txt')
    const data = await fsPromises.readFile(file)
    console.log(data.toString())
  } catch (e) {
    console.log(e)
  }
}

main()

This is a nice feature, which I think will frequently be used by developers from now on.

2. Promises in the AWS SDK

Most methods in the AWS SDK are asynchronous, and as such, they come with a callback.

2.1. With callback

Consider the following example when a data key is encrypted with KMS:

const AWS = require('aws-sdk')

const kms = new AWS.KMS({
  region: 'us-west-2'
})

const params = {
  KeyId: 'cb2c8f93-2562-489e-b612-abac83557c1d',
  Plaintext: privateKey.toString('hex')
}

kms.encrypt(params, (err, data) => {
  if (err) throw err
  console.log(data)
})

data should contain the encrypted text and the encryption key.

2.2. Use promise

These methods in general can be converted to a Promise object:

const encryptedPromise = kms.encrypt(params).promise()
enrcyptedPromise()
  .then((data) => {
    console.log(data)
  })
  .catch((e) => {
    console.log(e)
  })

The promise method is available and chainable to almost all SDK methods, and makes the use of the SDK easy. Similarly to the fs module, async functions can also be used.

3. Wrap it in a Promise

But what if the promise method is not available at the method? It’s rare, but happens. For example, the getSignedUrl method of the s3 module doesn’t support the conversion.

In this case, if it’s really important to convert the callback to Promise, we can wrap everything into a Promise:

const AWS = require('aws-sdk')

const s3 = new AWS.S3({
  region: 'us-west-2',
  signatureVersion: 'v4',
})

const params = {
  Bucket: NAME OF THE BUCKET, // name of the bucket the file is to be uploaded
  Key: NAME OF THE OBJECT, // the name of the file including the folder
  Expires: 60, // the link expires after 1 minute
}

const signedUrlPromise = () => {
  return new Promise((res, rej) => {
    s3.getSignedUrl('putObject', params, (err, url) => {
      if (err) rej(err)
      res(url)
    })
  })
}

signedUrlPromise()
  .then((url) => {
    console.log(url)
  })
  .catch((e) => {
    console.log(e)
  })

Alternatively, we can use promisify from util:

// require other dependencies
const { promisify } = require('util')
// initiate s3
const getSignedUrlPromise = promisify(s3.getSignedUrl).bind(s3)
// define params
getSignedUrlPromise('putObject', params)
  .then((data) => {
    console.log(data)
  })
  .catch((e) => {
    console.log(e)
  })

This is a nice and consise syntax. promisify converts error-first type callbacks which are the last parameters of the relevant method to much better manageable promises.

4. Third-party libraries

As we have seen, Node has a good support for promises now. But if, for some reason, this is not enough, third-party promise libraries can be used. From my experience, these libraries more often occur in legacy code with lower Node versions, and developers (including myself) prefer native methods over adding dependencies.

Bluebird is a widely-used promise library, which can transform the methods of entire callback-based modules to promises.

The promisifyAll method is one of the more often used features of Bluebird, which just does exactly that. Please refer to the links for more information on how to use Bluebird.

5. Summary

Callbacks are hard-to-work-with constructions. Promises, and especially async/await are more modern, and they are considered as best practice in new code.

NodeJS comes with the promisify method, which converts error-first callbacks to promises. Alternatively, the fs module comes with the promises method, which is stable in version 12.

AWS SDK provides the option to convert the callback-style methods to promise-based ones for most methods.

Thanks for reading, and see you next time.