Using promises for NodeJS and AWS SDK methods
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.