Skip to content

Instantly share code, notes, and snippets.

@BrianWill
Last active April 26, 2024 08:11
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save BrianWill/d7e02cdeb8d9c43152750c52887418ff to your computer and use it in GitHub Desktop.
Save BrianWill/d7e02cdeb8d9c43152750c52887418ff to your computer and use it in GitHub Desktop.
Using asynchronous API's using callbacks and Promises

In most programming languages, most functions are synchronous: a call to the function does all of its business in the same thread of execution. For functions meant to retrieve data, this means the data can be returned by calls to the function.

For an asynchronous function, a call to the function triggers business in some other thread, and that business (usually) does not complete until after the call returns. An asynchronous function that retrieves data via another thread cannot directly return the data to the caller because the data is not necessarily ready by the time the function returns.

In a sense, asynchronous functions are infectious: if a function foo calls an asynchronous function to conclude its business, then foo itself is asynchronous. Once you rely upon an asynchronous function to do your work, you cannot somehow remove the asynchronicity.

callbacks

When the business initiated by an asynchronous function completes, we may want to run some code in response, so the code running in the other thread must do something to signal when it has finished. Most commonly, this is done by invoking a callback function that was passed to the asynchronous function. When invoking the callback, the other thread passes in the data (if any) and possibly an error value (indicating the operation failed).

/* calling a synchronous function that retrieves data */
var foo = getData();  // the data is returned from the function
/* calling an asynchronous function that retrieves data */
var bar;
asyncGetData(function (err, data) {
    if (err) {
        console.log(err);  // we can't have the data, for some reason  
        return;
    }
    bar = data;
});

Because the callback runs at some later time, the bar variable here will always remain undefined immediately after the call to asyncGetData.

Effectively, everything which we want done after an asynchronous function call completes its business must be placed in the callback.

chaining callbacks

When calling synchronous functions in series, data returned from one can be fed into the next:

var dataX = syncX();
var dataY = syncY(dataX);
var dataZ = syncZ(dataY);
doSomething(dataZ);

But when calling asynchronous functions in series, the data they 'return' is only available in the callback, so each call must be done in the callback of the previous call:

asyncX(function (dataX) {
    // the call to asyncY() needs the data retrieved by asyncX()
    asyncY(dataX, function (dataY) {
        // the call to asyncZ() needs the data retrieved by asyncY()
        asyncZ(dataY, function (dataZ) {
            // the call to doSomething() needs the data retrieved by asyncZ()
            doSomething(dataZ);
        });
    });
});

Nesting functions to several levels can get ugly, but we can clean things up a bit by making the callbacks named functions:

asyncX(asyncXCallback);

function asyncXCallback(data) {
    asyncY(data, asyncYCallback);
}

function asyncYCallback(data) {
    asyncZ(data, asyncZCallback);
}

function asyncZCallback(data) {
    doSomething(data);
}

This pattern has its own problems: the code is now polluted with more functions for which it's difficult to give helpful names, and the sequential flow of the program is chopped up at arbitrary points.

Whether you use named functions or anonymous functions, it's very difficult to handle exceptions properly with callbacks. When a function throws an exception, the calling context is the appropriate place to handle the problem, but an exception thrown in a callback is never propagated to the calling context.

promises

To avoid an ugly series of nested callbacks and to better handle errors, we have an alternative to callbacks called Promises. Instead of taking a callback, an asynchronous function can return a Promise, a value representing the to-be-completed business of the call. Using the returned Promise's .then() method, we register functions to be called when the to-be-completed business completes (or called immediately if the business has already completed).

var p = asyncX();  // asyncX returns a Promise
// .then() takes two functions: a success handler and a failure handler
p.then(
    function (data) {
        // the async operation succeeded
    },
    function (err) {
        // the async operation failed
    }
);

At some point, the Promise resolves, either successfully or not, and either the sucess or failure handler is called (but never both). If the Promise has already resolved before the .then() call, one of the handlers is invoked immediately by the .then() call.

We can call .then() on a single Promise as many times as we like. When a Promise resolves successfully, all registered success handlers run (in no particular order); when a Promise resolves unsucessfully, all registered failure handlers run (in no particular order).

Every call to .then() returns a new Promise. We can effectively form chains of Promises:

var p = asyncX();  // asyncX returns a Promise
// .then() takes two functions: a success handler and a failure handler
p.then(
    function (data) {  
        console.log(data);   // the value resolved by the original Promise
        return 3;
    },
    function (err) {
        console.log(err);    // the error value resolved by the original Promise
        throw 'hi';
    }
).then(
    function (data) {
        console.log(data);   
        return 5;
    },
    function (err) {
        console.log(err);      
        throw 'yo';
    }
).then(
    function (data) {
        console.log(data);      
    },
    function (err) {
        console.log(err);    
    }
);

If the original Promise resolves unsuccessfully, the failure handlers execute in the chained order. For the code above, this would mean printing: the error from the original Promise, then 'hi', then 'yo'.

As each Promise resovles successfully, the next success handler is called with the prior return value. So the code above upon success would print: the data from the original Promise, then 3, then 5.

If an exception is thrown from a success handler, the remaining failure handlers execute in the chained order:

var p = asyncX();  // asyncX returns a Promise
p.then(
    function (data) {  
        console.log(data);   // the value resolved by the original Promise
        throw 'oh no!';
        return 3;
    },
    function (err) {
        console.log(err);    // the error value resolved by the original Promise
        throw 'hi';
    }
).then(
    function (data) {
        console.log(data);   
        return 5;
    },
    function (err) {
        console.log(err);    
        throw 'yo';
    }
).then(
    function (data) {
        console.log(data);      
    },
    function (err) {
        console.log(err);   
    }
);

For the above code, if the original Promise resolves successfully, this would print: the data from the original Promise, then 'oh no!', then 'yo'.

If an exception propagates out of a failure handler, the next error handler will get the propagated exception value.

Here's a simple way to think about it: when a Promise resolves successfully, execution goes down the happy path until an exception propagates out of a success handler, in which case execution will continue down the sad path; when a Promise resolves unsuccessfully, execution will go down just the sad path. Once we're on the sad path, we stay on the sad path.

chaining async operations with Promises

So far, the chain of .then() handlers run one-after-the-other without waiting. However, when a success handler itself returns a Promise, the next .then() in the chain will not run until the Promise resolves:

var p = asyncX();  // asyncX returns a Promise
p.then(
    function (data) {  
        return asyncY(data);   // asyncY returns a Promise
    },
    function (err) {
        throw err;
    }
).then(
    function (data) {
        return asyncZ(data);    // asyncZ returns a Promise
    },
    function (err) {
        throw err;
    }
).then(
    function (data) {
        doSomething(data);      
    },
    function (err) {
        console.log(err); 
    }
);

In the above code, the handlers of the second then will not run until the Promise returned by asyncY() resolves. If it resolves successfully, the success value is passed to the next success handler; otherwise, if it resolves unsuccessfully, the error value is passed to the next failure handler.

Effectively now, doSomething() will wait for the data from asyncZ(), which will wait for the data from asyncY(), which will wait for the data from asyncX().

Be clear however that the original Promise and everything chained from it still all run asynchronously—they just run in sequential order, each waiting for the previous step to finish before starting. Whatever statements of code come after this Promise chain will always run before the first .then() handler does.

omitting handlers in .then()

If you pass null or undefined instead of a success or failure handler, execution will simply continue down the chain:

var p = asyncX(); 
p.then(
    function (data) {  
        return asyncY(data);   
    },
    undefined                  // no error handler: skip to next error handler
).then(
    null,                      // no success handler: skip to next success handler
    function (err) {
        throw err;
    }
).then(
    function (data) {
        doSomething(data);      
    },
    function (err) {
        console.log(err); 
    }
);

Most commonly, we want to handle the errors of a chain all at the very end, in which case we can simply omit error handlers from all but the last .then(), and for this last .then() we can simply omit the success handler:

var p = asyncX(); 
// the second param of this call to .then() will be undefined
p.then(           
    function (data) {  
        return asyncY(data);   
    }
).then(
    function (data) {
        return asyncZ(data);    
    }
).then(
    function (data) {
        doSomething(data);      
    }
).then(
    null,
    function (err) {
        console.log(err); 
    }
);

This pattern is so common that Promises have a minor convenience method, .catch(), which is just like .then() but only takes a failure handler. So typically, the last method in the chain is .catch():

asyncX().then(    
    function (data) {  
        return asyncY(data);   
    }
).then(
    function (data) {
        return asyncZ(data);    
    }
).then(
    function (data) {
        doSomething(data);      
    }
).catch(
    function (err) {
        console.log(err); 
    }
);

creating promises

Finally, what if the asynchronous API we use expects callbacks instead of returning Promises? Well we can wrap the asynchronous functions to return Promises we create ourselves. Here's how a Promise might be created:

  1. Create a Promise.
  2. Invoke the asynchronous function, passing in a callback.
  3. In the callback, call the Promise's .resolve() method upon success, passing in the data, or call the Promises's .reject() method, passing in the error.
// WARNING: an ES6 Promise object doesn't actually have .reject() or .resolve() methods
// Wrap the asyncReadFile() function to return a Promise.
function promiseReadFile(filepath) {
    var p = new Promise();        
    asyncReadFile(filepath, function (err, data) {
        if (err) {
            p.reject(err);   // trigger the Promise's failure handlers
            return;
        }
        p.resolve(data);     // trigger the Promise's success handlers
    });
    return p;
}

The standard ES6 Promise object is a little less straight-forward:

// Wrap the asyncReadFile() function to return a Promise.
function promiseReadFile(filepath) {
    return new Promise(
        function (resolve, reject) {
            asyncReadFile(filepath, function (err, data) {
                if (err) {
                    reject(err);  // trigger the Promise's failure handlers
                    return;
                }
                resolve(data);    // trigger the Promise's success handlers
            });
        }
    );
}

The function passed to new Promise() is immediately invoked and passed two functions: call the first to resolve the Promise; call the second to reject the Promise. (I believe the reason for this odd arrangement is that it discourages resolve() and reject() from being invoked from other parts of code. Only the original async function's callback should decide whether the Promise resolves successfully or not.)

ES6 Generators

ES6 provides a new kind of function called a generator. A generator can pause and resume, returning multiple results incrementally: a call to a generator returns an iterator; calling .next() on the iterator gets the next value.

Generators are useful as an alterative to promises and conventional callbacks, as explained here and here.

ES7 await

The await keyword is a feature proposed for ES7. If adopted, this feature will make working with Promises more convenient and less verbose.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment