Pragmatism in the real world

Using async/await in OpenWhisk

I’m currently writing an OpenWhisk action in JavaScript that searches Twitter using their API. To do this, I need to get a bearer token from one API endpoint and then call the search endpoint.

Disclaimer: I’m in no way a JavaScript expert, so I would love it if you could constructively suggest improvements in the comments!

To do this in OpenWhisk, you need to use Promises like this:

var request = require('request-promise');

function main(params) {
    return new Promise(function(resolve, reject) {
        // get Bearer token
        request({
            "method":"POST",
            "uri": "https://api.twitter.com/oauth2/token",
            "json": true,
            "headers": {
              "Authorization": "Basic " + params.basic_token
            },
            formData: {
                grant_type: "client_credentials"
            }
        }).then(function (result) {
            // extract the access token from result
            console.log("Retrieved access token")
            return result.access_token
        }).then(function (access_token) {
             // do Twitter search
             request({
                "method":"GET",
                "uri": "https://api.twitter.com/1.1/search/tweets.json?q=%23oscars",
                "json": true,
                "headers": {
                  "Authorization": "Bearer " + access_token
                }
            }).then(function (results) {
                console.log("Found tweets")
                resolve({tweets: results.statuses})
            })
        })
        .catch(function(err) {
            console.log(err)
            reject({"error": "We failed!"})
        })
    })
 }

I’m assured this is better than the pyramid of doom, but urgh!

It works, but it’s not pretty and it’s hard to reason about what’s going on. Let’s start by separating out into functions.

Creating two functions is easy enough. We’ll call them getBearerToken() and searchTwitter(). This is getBearerToken():

 function getBearerToken(basic_token) {
    return request({
        "method":"POST",
        "uri": "https://api.twitter.com/oauth2/token",
        "json": true,
        "headers": {
          "Authorization": "Basic " + basic_token
        },
        formData: {
            grant_type: "client_credentials"
        }
    }).then(function (result) {
        // extract the access token from result
        console.log("Retrieved access token")
        return result.access_token
    })
}

As you can imagine, searchTwitter() is very similar. We can then write our main() function like this:

function main(params) {
    return new Promise(function(resolve, reject) {
        // get Bearer token
        getBearerToken(params.basic_token)
        .then(function (bearerToken) {
            // do Twitter search
            return searchTwitter(bearerToken)
        })
        .then(function (results) {
            console.log("Found tweets")
            resolve({tweets: results.statuses})
        })
        .catch(function(err) {
            console.log(err)
            reject({"error": "We failed!"})
        })
    })
}

Clearer, but there’s still a lot of cruft as it’s still quite hard to notice that the action’s output is the resolve({tweets: results.statuses}) statement buried in the middle of the function.

Async/await

Turns out that there’s something called async/await that hides all that Promises stuff away from you and makes it all more readable!

Essentially await pauses execution until the promise has resolved and async tells the system that this function is asynchronous.

We can refactor getBearerToken() to be an async function like this:

async function getBearerToken(basic_token) {
    const result = await request({
        "method":"POST",
        "uri": "https://api.twitter.com/oauth2/token",
        "json": true,
        "headers": {
          "Authorization": "Basic " + basic_token
        },
        formData: {
            grant_type: "client_credentials"
        }
    })

    console.log("Retrieved access token")
    return result.body.access_token
}

This is much clearer and easier to reason about. The await before the request() call means that we can forget about then() and just put the code to execute after the request has completed immediately after. We then mark the function as asynchronous by prefixing the declaration with async.

Similarly, our main() function becomes simpler too:

async function main(params) {
    try {
        const access_token = await getBearerToken(params.basic_token)
        var results = await searchTwitter(access_token)
    } catch (err) {
        console.error(err)
		return {error: "We failed!"}
    }

    return {tweets: results.statuses}
}

Again, we mark our function as async and then we don’t need to instantiate a Promise and lose all the then() calls. As a result it’s much easier to see what’s going on and we can easily see what this action returns.

Another nice feature of await is that the error can be caught using a standard JS catch statement.

A quick note on returning an error

In OpenWhisk, if you return a dictionary with an key called error, then the action has failed:

return {error: "We failed!"}

This is really clean, but requires you to know that error is special.

Behind the scenes, OpenWhisk is using Promises to call our function so we can return a resolved Promise. This means that we can use Promise.reject() if we wanted to:

return Promise.reject("We failed!")

If we do it this way, then the OpenWhisk JS runtime will convert it to {error: "We failed!"} for us which some may find clearer.

Fin

All in all, I think async/await makes for much clearer and easier to read code. To use it with OpenWhisk, make sure you use the nodejs:8 runtime by using the --kind parameter to wsk:

$ wsk action update --kind nodejs:8 twitter-search twitter-search.js

One thought on “Using async/await in OpenWhisk

Comments are closed.