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

In the follow-on to What Is Asynchronous Programming?, we start from  the idea:
Asynchronous code is inevitable if we insist on the simplicity of a single threaded UI.

This is the second of two articles on asynchronous programming:

  1. What Is Asynchronous Programming?
  2. Managing Asynchronous Code - Callbacks, Promises & Async/Await

Asynchronous code is inevitable if we insist on the simplicity of a single threaded UI. 

There are, however, other reasons for using asynchronous code such as efficiency. For example, Node.js is an asynchronous alternative to a multi-threaded web server like Apache. The argument is that async is appropriate as some sort of super light cooperative multi-tasking. This is another argument and for the rest of this article it will be assumed that the use of asynchronous code is necessary due to the more common demands of the single threaded UI. 

Note that we already have our first "solution" to the asynchronous code problem - make the UI multi-threaded. In this case there is no need to write callbacks or anything similar because the thread dealing with the event can just enter a wait state until the long running task is complete. This is generally not thought to be a practical alternative because multi-threading a UI is difficult and in the past has lead to disaster. 

For the time being the single threaded UI is going to be the norm and asynchronous code is here to stay - for a long while at least. 

The situation is most acute in languages such as JavaScript which are, until very recently, completely single threaded languages. In JavaScript, until the advent of worker threads, there was only the UI thread to do any work with. 

As a result many of the innovations in asynchronous code have been due to JavaScript. But arguably not the most important of all - async and await. 

But this is getting to the end before we have completed the beginning. 

What exactly is the problem we are trying to solve?

Many of the issues that arise with asynchronous programming have been discussed in the previous article - What Is Asynchronous Programming? You will hear lots of explanation of the problem of asynchronous code along the lines of "callback hell", and the "callback pyramid of doom". These are problems but they arise from taking particular approaches to asynchronous programming. 

The first problem is that raw asynchronous programming distorts the intended flow of control.

In a synchronous program you might write

loadA();
loadB();
loadC();

and you can expect A to load before B which loads before C.

As soon as you convert these to async operations you can't be sure what order things are done in unless you adopt the callback cascade:

loadA(loadB(loadC()));

where each accepts a callback as its parameter. The callback approach to async turns sequential default flow of control into nested function calls. But keep in mind that the callback approach is just one of many. Because it is so widely used there is a tendency to think that a callback is the only way to deal with asynchronous code. 

Asynchronous code is essentially about co-operative multitasking with a single thread. 

More Threads?

This leads us to our first possible solution - introduce more threads. This isn't an approach that was possible in JavaScript until worker threads were introduced but it is common in languages such as Java, C# and so on. 

The idea is very simple. 

Every time you write an event handler make it start a new thread and write all of the event handler code there. In this approach the UI thread only serves to signal to the event handler that it is needed and the event handler starts a new thread and returns at once. 

This is a very standard approach to things like animation loops. The animation loop is often started by a user action which calls an event handler that spins up a timer which runs code on another thread every so often. 

If you take this approach - every event handler is a new thread. You don't have an asynchronous problem but you do have a UI access problem. More generally you have parallel programming problems. This is why the UI isn't multithreaded by default. 

Now if any event handler's new thread attempt to change the UI it will generally result in an exception. The reason is that most UI frameworks check to make sure that the thread trying to access a component is the UI thread. This is to ensure that multiple updates can't occur and that there are no race problems. 

The general solution to this is to provide a function something like runOnUIThread() which defines a function that will be run on the UI thread within the code that is running on the new thread. 

This works but you have just invented something very like a callback only from the non-UI thread to the UI thread. 

However don't write this approach off too quickly. 

For simple tasks the runOnUIThread code can be very small and simple - often just one instruction updating some part of the UI. The overall structure can seem simpler calling the UI from the non-UI thread. There is also the advantage that the non-UI thread determines when to call the UI update and so this integrates better with the logic of the task. That is the non-UI thread is closer to the simple synchronous code that you would have written if possible. 

Even so the Callback is still the most common way of tackling the problem.

Asynchronous Flow Of Control

The effect that asynchronous code has on the structure of your program is easy to understand in terms of what code goes before and what code goes after:

simple

The statements that are before the asynchronous operation provide the context for the call. Variables which are set in this before code may contain values that are important for what to do with the result of the asynchronous operation in the code to follow it. The code that follows it makes use of the result of the operation and the context established by the earlier code. 



Last Updated ( Wednesday, 23 March 2022 )