Javascript Promises, Async & Await

My post “Develop Lambda with Node.js” presents my first attempt to learn and use Node.js and subsequently Javascript. Coming to Javascript with a Java background had to spend some time to familiarize my self with the asynchronous concepts.

Node.js uses an event-driven, non-blocking I/O model, which means that the execution of the code will not block waiting for and I/O operation to complete. In order to succeed this, Javascript have the concept of callback functions. In Javascript, functions are objects. Due to that, functions can take other functions as arguments and can be returned by other functions.

By looking at the code in the lambda function from the previous post we can see that both API operations against Cognito accept two arguments:

  • an AdminGetUserRequest
  • a callback function that will be called when the I/O operation completes, this function will handle the success or failure of the operation.

By one API operation needing the result of the other operation, we have ended up with nested callback calls, which is also known as callback hell.

Promises

According to MDN web docs a Promise is an object that represents the eventual completion or failure of an asynchronous operation and it’s returning value. There are two aspects on understanding Javascript promises, handling promises and creating promises. Since AWS SDK supports promises when calling service operations, in the first part of this blog post we will explore the handling of promises.

What are the Promise’s main characteristics?

A Promise in any given time of the code execution can be in one of the below states:

  • fulfilled - the code related to the promise has actually succeed.
  • rejected - the code related to the promise has failed/error.
  • pending - is neither fulfilled or rejected yet.
  • settled - has fulfilled or rejected.

When a promise has settled either by succeeding or failing, it can not be resettled. A settled promise is immutable. Essentially, a promise is an object to which you attach callbacks, instead of passing callbacks into a function. The Promise object provides three protype methods:

  • then() - gets assigned two handlers, one for the success value and one for the rejected value. The second one is optional.
  • catch() - gets assigned a handler for the rejected value.
  • finally() - gets assigned a handler to be executed irrespective is the Promise succeeded or failed.

All three prototype functions return a Promise and therefore can be chained.

Having this basic introduction in Promises, the next step is to try and refactor the Lambda Code. As per the Support for Promises in SDK, instead of using callbacks, the AWS.Request.promise() method provides a way to call a service operation and return a promise. Below is the transformed function:

exports.handler = function (event, context, callback) {
    if (event.email) userRequest.Username = event.email;

    cognito.adminGetUser(userRequest).promise()
        .then( function (adminGetUserResponse) {
            console.log("The response from the user is: ", JSON.stringify(adminGetUserResponse));
            cognito.adminDeleteUser(userRequest).promise()
                .finally(function (deleteUserResponse) {
                    console.log(JSON.stringify(deleteUserResponse));})
        }).catch(function (errorValue) {
        console.log("Erron on fetching the user:", JSON.stringify(errorValue))
    });

    return callbak(null, response);
};

When executing this function we will notice that when adminGetUser.promise() is resolved the internal adminDeleteUser.promise() is being ignored. This happens because once a Promise has resolved, the then() function will be called and it can not resolve again. This lead for me to read the documentation more thoroughly:

Promise.prototype.then(): Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler, or to its original settled value if the promise was not handled (i.e. if the relevant handler onFulfilled or onRejegcted is not a function).

So, in order for the adminDeleteUser.promise() to not be ignored, we need to return it in the then() function of the first promise and handle it’s resolvement or rejection in a chained then(). So the function is transformed as below:

exports.handler = function (event, context, callbak) {
     if (event.email) userRequest.Username = event.email;

     cognito.adminGetUser(userRequest).promise()
         .then(function (adminGetUserResponse) {
             if (adminGetUserResponse && adminGetUserResponse['UserStatus'] === 'UNCONFIRMED') {
                 return cognito.adminDeleteUser(userRequest).promise();
             } else {
                 return new Promise(function (resolve, reject) {
                     reject("User does not fulfill requirements for deletion ");
                 })
             }
         })
         .then(function (response) {
             console.log("User deleted: ", JSON.stringify(response));
         })
         .catch(function (errorValue) {
             console.log("Error :", JSON.stringify(errorValue))
         });

     return callbak(null, response);
 };

If the user fulfills the criteria for deletion we return the adminDeleteUser.promise(), otherwise we will return a new Promise that rejects with a message for the developer.

Next Step: Async / Await

The usage of promises has improved a lot the readability of the code, compared to the nested callbacks and the nested error handling. On top of promises, someone can also use async and await pattern. All three - callbacks, promises and async/await handle asynchronous execution.

As per MDN the Asynchronous function declaration defines an asynchronous function, which when called it will return a Promise, which eventually will resolve or reject.

The Lambda programming model for Node.js 8.10 supports defining an async function handler. Given that we can transform the lambda as follows:

exports.handler = async (event) {
    if (event.email) userRequest.Username = event.email;

    [...]

    return response;
};

By executing this piece of code we will observe that the requests to cognito service are not actually executed. That’s because the async handler will return immediately the response object wrapped in a promise. We need to introduce await in order to pause the execution of the async function and wait for the cognito service’s promise resolution, and then resumes the async function’s execution and returns the resolved value:

const aws = require('aws-sdk');
const cognito = new aws.CognitoIdentityServiceProvider({apiVersion: 'latest', region: 'REGION'});

const userRequest = {
    UserPoolId: 'STRING-POOL-ID',
    Username: '',
};

const response = {
    statusCode: 200,
    body: JSON.stringify({
        message: 'OK',
    }),
};

function adminDeleteUser(user) {
    if (user && user['UserStatus'] === 'FORCE_CHANGE_PASSWORD') {
        return cognito.adminDeleteUser(userRequest).promise();
    } else {
        return new Promise(function (resolve, reject) {
            reject("User does not fulfill requirements for deletion ");
        })
    }
}

exports.handler = async function (event, context) {
    if (event.email) userRequest.Username = event.email;
    try {
        const user = await cognito.adminGetUser(userRequest).promise();
        await adminDeleteUser(user);
    } catch (err) {
        console.log('Error: ' + JSON.stringify(err));
    }
    return response;
};

In more simple words the await is used for calling an asynchronous function and waits for it to resolve or reject. It will block the execution of the code within the asynchronous function in which it is located.

Conclusion

This Lambda function was my stepping stone to get introduced in Javascript, Node.js and the asynchronous patterns these tools provide and lead me to look for the asynchronous programming in the Java environment. Stay tuned …

P.S.

The full setup of the lambda with all the examples presented resides in this repo.