Wednesday, 26 April 2017

How to chain an ES6 Promise

Node.js uses async functions extensively, as it based around non-blocking I/O. Each function takes a callback function parameter, which can result in some messy, deeply nested callback functions if you have to call a bunch of async functions in sequence. Promises make these callbacks a lot cleaner.

ES6 (or ES2016) Promises are a great way of chaining together asynchronous functions so that they read like a series of synchronous statements.

There's already some great posts on Promises, 2ality has a good intro to async then detail of the api, so I won't rehash that article. However, after starting to use them for cases more complicated than most examples, it easy to make a few mistaken assumptions or make things difficult for yourself.

So here is a more complicated example showing a pattern I like to follow. In this example, I'll use the new Javascript Fetch API, which is an API that returns a Promise, allowing you to make async HTTP calls without having to muck around with XMLHttpRequest calls.

First off there are 3 ways to start the chaining, the most obvious one is (taken from MDN, updated to use arrow functions):
function callPromise() {
  return fetch('flowers.jpg');
}
var myImage = document.querySelector('img');
callPromise()
  .then(response => response.blob())
  .then(myBlob => {
    var objectURL = URL.createObjectURL(myBlob);
    myImage.src = objectURL;
  });
A slightly different:
function callPromise() {
  return fetch('/data');
}
function handler1(body) {
  console.log("Got body", body);
}
Promise.resolve()
  .then(callPromise)
  .then(response => response.json())
  .then(handler1);
It starts the chain with a promise that immediately calls the next then in the stack. The benefit of this, is that all the statements that perform an action are in the 'then' invocations, so your eye can follow it easier. I personally prefer this way, as I think its to read but both are effectively equivalent.
There is one minor difference that you should be aware of when doing it this way. In the first example callPromise is called immediately when the javascript engine gets to that line. In the 2nd example callPromise is not called until the javascript engine gets to the end of the call stack - it gets called from the event loop.
The 3rd way is by creating a new Promise(). For this article lets just stick to consuming a promise.

The response for each 'then' be any object or undefined which is then passed as the only argument to the next function in the chain. You can also return a promise (or a 'thenable' function) who's final output is used to as the parameter for the next function. So you don't have to resolve any response you return, the promise library automatically normalises this behaviour for you.

A Catch Gotcha 

Once a promise is in a rejected state it will call all 'catch' handlers from that point forward. The 'then' function can take both a onFulfilled and a onRejected parameter and it can be easily mistaken which handlers are called. Looking at the following example, if fetch throws an Error then errorHandler1, errorHandler2 and errorHandler3 will all be called.
Promise.resolve()
    .then(() => fetch('url1'), errorHandler1)
    .then(response => fetch('url2'), errorHandler2)
    .then(response => fetch('url3'), errorHandeler3);
So how do you achieve what you were intending if you did the above? The answer is to add the errorHandler to the promises returned in each of the fulfilled handlers before they get added to the outer promise chain. An example exlpains it a lot better, applying it to the above:
Promise.resolve()
    .then(() => {
        return fetch('url1')
            .catch(errorHandler1);
    })
    .then(response => {
        return fetch('url2')
            .catch(errorHandler2);
    })
    .then(response => {
        return fetch('url3')
            .catch(errorHandeler3)
    })
    .catch(finalErrorHandler);
In the above, only one the errorHandlers 1-3 only get called if the individual fetch call fails. If one of the fetch fails then the finalErrorHandler is called, so you could use it as a single place to return an error response up the async stack.

Continuing a Promise After an Error

Usually if a Promise chain goes into an error state, it will call all the error handlers and never call any more of the fulfilled handlers. If you want the fulfilled handlers to be continued to be called when the error can be recovered from, then you need to return a Promise in a fulfilled state.
Promise.resolve()
    .then(() => {
        throw new Error();
    })
    .catch(err => {
        // handle recoverable error
        return Promise.resolve(); // returns a Promise in a fulfilled state
    })
    .then(() => {
        // this handler will be called
    });

Making Testable Promises

Using arrow functions in promises makes for pretty code in blog posts but if the promises are implemented this way it makes them hard to test. In order to test each handler function, you have to call the entire chain, which for anything other than the most trival chain is tedious and makes for bad unit tests. So to make them testable, my recommendation is to not use arrow functions or function expressions in a promise chain. Instead create a named function for each handler that is exported so it can be invoked from a test case. There are cases where it make sense to use a one line arrow function but use sparingly. I'll post a complete example in another post.

Using a Promise to Once-only Init

Promises can only settle once, calling the attached stack once it resolves only once. Additionally, calling 'then' or 'catch' on an already settled promise, will immediately can the passed function. This can make it handy to implement asynchronous init events, for instance a AWS Lambda function that queries DynamoDB as soon as it boots up and then finishes the initialisation on the the first request that it handles using environment variables stored in API Gateway.
import aws  from 'aws-sdk';
import SpotifyWebApi from 'spotify-web-api-node';
const dynamoDB = new aws.DynamoDB.DocumentClient({region: 'ap-southeast-2'});

var apiLoadedResolve;
var apiLoadedPromise = new Promise((resolve, reject) => {
    apiLoadedResolve = resolve;
});

export function lambdaHandler(request, lambdaContext, callback) {
    let api = getApi(request);
    // handle individual request
    return {}; // response object
}

var spotifyApi;
function getApi(request) {
    if (spotifyApi)
        return spotifyApi;

    spotifyApi = new SpotifyWebApi({
        clientId: request.env.SPOTIFY_CLIENT_ID,
        clientSecret: request.env.SPOTIFY_SECRET,        
    });
    // this only triggers the promise to be settled once
    apiLoadedResolve(spotifyApi);

    return spotifyApi;
}

// access dynamo db on script load before lambdaHandler is called
dynamoDB
    .get({
        TableName: "TestTable",
        Key: {name: "initData"}
    })
    .promise()
    .then(item => {
        let conf = item.Item;
        apiLoadedPromise = apiLoadedPromise.then(spotifyApi => {
            // passed in on first request
            spotifyApi.setAccessToken(conf.access_token);
            spotifyApi.setRefreshToken(conf.refresh_token);
        });
    });

This implementation doesn't require the config from the database to be set before being able to process requests. If you needed the config to be set before processing requests add a conditional check to see if it's init'ed, if not execute the rest of the function as a then callback to apiLoadedPromise

No comments:

Post a Comment