Introducing async JavaScript

Draft
This page is not complete.

In the first article of this module we look at what synchronous and asynchronous code are (sync and async), and how they differ. We'll also examine traditional sync methods of handling a sequence of operations, see what the problems are, and begin to understand how async techniques can help us solve such problems.

Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective: To gain familiarity with what asynchronous JavaScript is, how it differs from synchronous JavaScript, and what use caes it has.

Synchronous JavaScript

To allow us to understand what asynchronous code is, we ought to start off by making sure we understand what synchronous code is.

A lot of the functionality we have looked at in previous modules is synchronous (or sync) — you run some code, and the result is returned as soon as the browser can do so. Let's look at a simple example (see it live here, and see the source):

let pElem = document.createElement('p');
pElem.textContent = 'This is some example text';
document.body.appendChild(pElem);

const btn = document.querySelector('button');
btn.addEventListener('click', () =>
  alert('You clicked me!')
);

In this block, the lines are executed one after the other:

  1. We create a <p> element.
  2. We give it some text content.
  3. We append the element to the document body.
  4. We grab a reference to a <button> element that was already available in the DOM.
  5. We add a click event listener to it so that when it is clicked, an alert message appears.

When each operation is being processed, nothing else can happen — rendering is paused. This is because client-side JavaScript is a single threaded (only one thing can happen at once), blocking (everything else is blocked until an operation completes) language.

So in the example above, you won't be able to click the button to get the alert until after the paragraph has been created. You probaly won't notice this however — the above operations will take a matter of milliseconds to complete, meaning that everything will be processed by the time you click the button.

There are however many things you'll want to do with JavaScript that will take longer to complete. What if, for example, you wanted to do a lot of processing in a single operation?

Let's look at another example (again, see this running live, and see the source code for the full JavaScript):

function expensiveOperation() {
  for(let i = 0; i < 1000000; i++) {
    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
    ctx.beginPath();
    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
    ctx.fill()
  }

  alertBtn.addEventListener('click', () =>
    alert('You clicked me!')
  );
}

const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
document.body.appendChild(canvas);
let ctx = canvas.getContext('2d');

let alertBtn = document.querySelector('.alert');
let fillBtn = document.querySelector('.fill');

fillBtn.addEventListener('click', expensiveOperation);

In this example we:

  • Create a {(htmlelement("canvas")}} element, set its width and height, append it to the document <body>, and get a reference to its 2D drawing context.
  • Get references to the two buttons already present in the DOM — one which (eventually) displays the alert box, the same as in the previous example, and one that's explained in the next bullet.
  • Add an event listener to the second button so that when clicked, it runs a function called expensiveOperation(). In this function we:
    • Draw a million blue circles on the canvas. Yes, you read that right — one million!
    • Add the event listener to the first button that when clicked displays the alert.

So what happens when we use this example? Well, we can't display our alert until we've clicked the second button to set up the event listener on the first button. However, when you click the second button it will draw a million circles on the canvas before it sets the aforementioned listener up, which can take quite a bit of time as it requires a lot of number crunching. The canvas drawing code is sync code, and so it blocks the addEventListener() call inside the expensiveOperation() function until it has finished drawing.

The effect is that if you click on the second button and then immediately try to click on the first button, nothing will happen, as the first button's event handler is not set up yet. It will take a few seconds before the drawing operation finishes on the canvas, it turns blue, and the first button will then produce the altert when clicked.

This helps to illustrate how sync code can be problematic — imagine if you were trying to create a complex user interface, but the user couldn't use any features of it until all the code it depended on had loaded? This would create a bad user experience.

Methods of writing sync code

Before we jump in to async code, it is also worth briefly looking at two standard ways of writing synchronous code in a way that allows you to have some control over the execution order: Callbacks and try...catch blocks.

Callbacks

Callbacks are functions that are passed as parameters to other functions. An example of a callback is the second parameter of EventTarget.addEventListener (as we saw in action above):

alertBtn.addEventListener('click', () =>
  alert('You clicked me!')
);

The first parameter is the type of event to be listened for, and the second parameter is a function that is invoked when the event is fired.

Another example is when we use Array.prototype.forEach() to loop through the items in an array (see it live, and the source):

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

In this example we loop through an array of Greek gods and print the index numbers and values to the console. Again, the expected parameter of forEach() is a callback function, which itself takes two parameters, a reference to the array name and index values

When we pass a callback function as a parameter to another function, we are only passing the function definition as the parameter — the callback function is not executed immediately. It is “called back” (hence the name) synchronously, somewhere inside the containing function’s body. The containing function is responsible for executing the callback function when needed.

You can write your own function containing a callback easily enough. For example (run it live, and see the source):

function greeting(name) {
  alert('Hello ' + name);
}

function processUserInput(callback) {
  let name = prompt('Please enter your name.');
  callback(name);
}

processUserInput(greeting);

Here we create a greeting() function that simply alerts a greeting to a name passed to it. However, we then create a processUserInput() function that takes a callback as a parameter. It prompts the user to enter their name, and then runs the callback, passing the entered name to it so it can alert a greeting as before.

This gives you an idea of the versatility of callbacks — not only does it allow you to control the order in which functions are run and data is passed between them, it also allows you to pass data to different fuctions depending on circumstance. So you could have different actions to run on the user details you want to process like greeting(), goodbye(), addToDatabase(), requestEmailAddress(), etc.

However, for all their usefulness, such callbacks are still sync. They are still blocking the tread when they run.

Try/catch blocks

Another way to write synchronous code is to create try...catch blocks. The idea behind try...catch blocks is to run instructions in sequence but also react appropriately if any of the commands fail.

Let's look at another example (see it running live, and the source), which will use the following JSON text snippet:

// data from the server
let json = '{"id":"007", "firstName":"James", "lastName": "Bond"}';

In the try...catch block below, we want to make sure that the data parses successfully and do something useful if there is an error, instead of just throwing an error and stopping.

try {
  // convert the text representation to a JS object
  let user = JSON.parse(json);
  // Log the results to console
  console.log(user.id); // 007
  console.log(user.firstName);  // James
  console.log(user.lastName); // Bond
} catch(err) {
  console.log('Error name: ' + err.name);
  console.log('Error message: ' + err.message);
}

So in the try block, we attempt to parse the string into a JavaScript object based on the JSON. If this succeeds, we log the object properties to the console.

If the parsing fails for any reason, the code in the catch block is run (try changing the json variable to an incorrect name, to see it in action) — in this case we just log the error name and message contained in the provided error object. A more advanced example might attempt to reconnecting to the database and try again, or perhaps ask the user to enter the data again if it wasn't complete or well formed. This will do for our simple example, however.

There is a third block that you can optionally include — finally. This will run regardless of whether the operation succeeded or failed. This gives us the option of closing database connections and doing other cleanup tasks your code needs to be left in a stable state. The below snippet shows an example:

try {
  // convert the text representation to JS object
  let user = JSON.parse(json);
  // Log the results to console
  console.log(user.id); // 007
  console.log(user.firstName);  // James
  console.log(user.lastName); // Bond
} catch(err) {
  console.log('Error name: ' + err.name);
  console.log('Error message: ' + err.message);
} finally {
  // This will always execute regardless of success or failure
  console.log('Query finished');
}

try...catch blocks are also sometimes useful for feature detection, for example:

try {
  // Run feature test
} catch(err) {
  // Run fallback code if test fails
}

Asynchronous JavaScript

For the reasons illustrated earlier related to blocking (and others besides), many WebAPI features now use async code to run, especially those that influence or fetch some kind of resource from an external device, such as fetching a file from the network, accessing a database on the server and returning data from it, accessing a video stream from a web cam, or broadasting the display to a VR headset.

Let's look at a quick example, from our Fetching data from the server article:

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

Note: You can find the finished version on GitHub (see the source here, and also see it running live).

The fetch() method is async. Async operations are put into an event queue, which runs after the main thread has finished processing and so does not block subsequent JavaScript code from running. The queued operations will complete as soon as possible then return their results to the JavaScript environment.

fetch() takes a single parameter — the URL of a resource you want to fetch from the network — and it returns a promise. The promise is an object representing the completion or failure of the async operation. It represents an intermediate state, as it were.

Note: Promises are not the only way to run async code in a browser; you'll learn about others throughout the course. We are just using a promise as an example here.

This is a slightly strange concept. Neither of the possible outcomes have happened yet, so the fetch operation is currently waiting on the result of the browser trying to complete the operation at some point in the future. We've then got three further code blocks chained onto the end of the fetch():

  • Two then() blocks. Both contain a callback function that will run if the fetch operation is successful, and each callback contains the result of the previous successful operation, so you can go forward and do something else to it. Each .then() block returns another promise, meaning that you can chain multiple .then() blocks onto each other, so multiple async operations can be made to run in order, one after another.
  • The catch() block at the end runs if any of the .then() blocks fail — in a similar way to try...catch, an error object is made available inside, which can be used to report the kind of error that has occured.

Note: You'll learn a lot more about promises later on in the module, so don't worry if you don't understand them fully for now.

Mixing sync and async

Let's explore what happens when we try mixing sync and async code, so we can further understand the difference. The following example is fairly similar to what we've seen before (see it live, and the source):

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');

The browser will begin executing the code, see the first console.log() statement (Starting) and execute it, and then initialize the image variable.

It will then move to the next line and begin executing the fetch() block but, because it is async and not blocking, it will move on with the code execution, finding the last console.log statement (All done!) and outputting it to the console.

Only once the fetch() block has completely finished running, will we finally see the second console.log() message (It worked ;)) appear. So the messages have appeared in a different order to what you'd expect:

  • Starting
  • All done!
  • It worked :)

In a less trivial code example, this could cause a problem — you can't include an async code block that returns a result, which you then rely on later in a sync code block. You just can't guarantee that the async function will return before the browswer has processed the async block.

To see this in action, try taking a local copy of the example, and changing the third console.log() call to the following:

console.log ('All done! ' + image + 'displayed.');

You should now get an error in your console instead of the third message:

TypeError: image is undefined; can't access its "src" property

This is because at the time the browser tries to run the third console.log() statement, the fetch() block has not finished running so the image variable has not been given a value.

Active learning: make it all async!

To fix the problem described above, the best way is to make the third console.log() statement run async as well. This can be done by moving it inside another .then() block chained onto the end of the first one, like we saw earlier on. Try doing this now.

Note: If you get stuck, you can find an answer here (see it running live also).

Conclusion

JavaScript is at its most basic a synchronous, blocking, single-threaded language — only one operation can be in progress at a time. With async operations, instead of getting blocked the code is pushed to an event queue that gets fired after the execution of all the other code. This means that you can let your code do several things at the same time without stopping or locking your main thread.

Whether we want to run code synchronously or asynchronously will depend on what we're trying to do.

There are times when we want things to load and happen right away. The callback for a click event handler is a good example.

If we're running an expensive operation like querying a database and using the results to populate templates it is better to push this off the main thread and complete the task asynchronously.

In this module

  • Introducing Async JavaScript
  • Async loops and intervals
  • Handling async operations gracefully with Promises
  • Easier async code with async and await

Document Tags and Contributors

Contributors to this page: chrisdavidmills
Last updated by: chrisdavidmills,