Managing Asynchronous Code - Callbacks, Promises & Async/Await
Written by Mike James   
Monday, 21 March 2022
Article Index
Managing Asynchronous Code - Callbacks, Promises & Async/Await
The Callback And The Closure
onFulfilled and onRejected
Async and Await

The Callback And The Closure

The callback is the most common approach to asynchronous code but it distorts the flow of control and potentially loses context. 

For example when you convert the asynchronous call to a callback all of the code that follow it - the "after" code - becomes the callback:callback

You can now see that the flow of control has been distorted - what was one function is now two. Don't worry too much about it at the moment but this is a complete picture of what can happen because if the Callback contains an asynchronous call you repeat the procedure of moving the "after" code into a callback - distorting the flow of control again. 

Not only that but in a simple programming environment the context is lost. As the "after" code is now a separate function it no longer has access to the variables contained in the "before" code. In short the callback can't perform an instruction like Text(1,i) because i isn't only out of scope it doesn't even exist. 

This is the context bottle neck problem that callbacks introduce into asynchronous code. All of the data that was available to "after" now has to be sent to the callback as a set of parameters. Of course this doesn't work in practice because the LoadA function will often be a library function and you wont have control over its parameters. In other words LoadA will probably pass its result to the callback function(result) but it wont pass any variables from the context - function(result,i) say. 

You might have already guessed that this problem can be largely overcome if the language supports closure. If the callback function is defined within the original function it keeps the context provided by the instructions that are obeyed before it is created. That is in say, JavaScript, the callback does have access to the variable i and can perform result=Text(1,i) because closure provides all of the variables that were defined when it was. 

There are many complex and esoteric explanations of what closure is and why you might want it but it is this automatic provision of context to a callback function that seems the most convincing. There are lots of other uses of closure but it is this one that you would invent closure for. 

Closures ensure that callbacks have their context.

Of course things can be more complicated. It could be that the asynchronous function is itself nested within a control structure. 

control

This means that you not only have code before and after the asynchronous call there is code around it. This code that surrounds the call can be thought of as being part of the before and after code - but in reality it is quite a different type of problem. You have to implement something that converts the control structure into callbacks with a similar behaviour and this is not easy to solve in a completely general way.  For example, one way to implement a general asynchronous loop is to convert it into a recursion - see What Is Asynchronous Programming?.

Promises

The callback is a continuation passing approach to asynchronous code - you pass where the program should continue from as a parameter, i.e. the callback. Promises or futures also have a long history in programming but rather than going into the computer science it is more realistic to focus on the sudden interest in promises caused by their adoption in JavaScript or ECMAScript 2015 to be precise. 

The biggest problem with trying to understand what promises give you is that most of the accounts start out by explaining the inner workings of a Promise object. This is very interesting and you can learn a lot about JavaScript by studying how Promise is implemented but it doesn't help much with how to make use of it. Even when accounts move on to using Promise they tend to spend a lot of time explaining how to convert asynchronous callback based functions into Promise based functions. Again this is interesting and essential if you you are going to make use of promises but it doesn't help you understand the key ideas that motivate wanting to use promises. 

So, no computer science, no implementation details and no adding promises to existing asynchronous code. Instead we are going to assume that we live in a world where all asynchronous code uses promises and it is just up to us to code using this fact.

Instead of accepting a call back the asynchronous function returns a Promise object. This can be in three possible states:

  • pending - the asynchronous operation is underway
  • fulfilled - the operation completed and its result is ready to use
  • rejected - the operation failed. 

Once a Promise object enters either the fulfilled or rejected state it doesn't change state again.

The eventual result of a promise is always a single value - which sounds restrictive until you know that it can be an object. 

For example:

var p1 = asyncFunc();

returns a Promise object imediately and gets on with whatever it is supposed to be doing to get the eventual result. 

It is often said that a Promise is a promise to return a result. 

So how do you get to process the result? 

The then method of the Promise object is the most common way to process the eventual result. 

For example:

p1.then(function(r){
 console.log(r);
});

When the Promise object enters the fulfilled state the functions registered with the then methods are executed with the result as their single parameter.

You can register multiple functions with a Promise object and they will be executed in the order that they were registered.

If a Promise object is in the fulfilled state when you register a function then it will be called almost immediately.

The reason why it is "almost" immediately is that all registered functions are called asynchronously and hence the function that is using the Promise has to end before any "then" functions are called.

All registered functions are called just once. 

For example:

var p1 = asyncFunc();
p1.then(function (r) { console.log("first"); });
p1.then(function (r) { console.log("second"); });
console.log("Finished");

WIll display

Finished
first
second

even if the asyncFunc is completed before the functions are registered. 

So far the Promise looks a lot like just a neater way of setting up a callback - and in many ways that really is all it is but there are some other advantages in doing things this way. 

The Promise object is returned immediately to the calling function and this can be used to set up complicated call sequences that will only happen long after the calling function has completed. 

For example you could set two async operations going using:

var p1 = asyncFunc1();
p1.then(function (r) { console.log("first"); });

var p2=asyncFunc2();
p2.then(function (r) { console.log("second"); });

 

in this case which of the two would finish first isn't determined and the messages will be printed on the console in any order.



Last Updated ( Wednesday, 23 March 2022 )