Skip to main content

Write Asynchronous JavaScript

Learning Objectives

After completing this unit, you’ll be able to:

  • Identify important asynchronous features in JavaScript.
  • Invoke functions asynchronously using setTimeout.
  • Write and invoke callback functions.
  • Write and invoke promise-based functions.

Think way back to when we first introduced the JavaScript engine. The engine has a single thread that does work, finishes, then has new work stuffed into it to start over again.

Of course, it’s critical that the thread not be blocked. 

Let’s see an example of this.

<html>
  <script>
    alert("Does JavaScript show first?");
  </script>
  <body>
    <p>
      Does HTML show first?
    </p>
  </body>
</html>

If you load this HTML page in your browser, you find that the alert pop up displays first and then blocks the display of the HTML. This is because the alert() function halts execution of the JavaScript thread until the user dismisses it. In short, when JavaScript blocks your browser, it is never a good user experience. 

The good news is, apart from a few legacy bits that linger like the alert() function above, JavaScript is an asynchronous language. 

Asynchronous JavaScript Is Everywhere

To begin the journey into asynchronicity, let’s revisit events and functions. Previously we looked at some HTML and JavaScript like this.

<!-- HTML -->
<button id="clicker">
//JavaScript
let button = document.getElementById("clicker");
button.addEventListener("click", handleClick);

In this example we added handleClick as an event handler to the click event emitted by the button. 

And there! We’ve already written some asynchronous JavaScript. 

When the event fires, all that happens is a new message is added to the queue. No event has the ability to take over the thread. Each event fired must get in the queue and wait its turn to run. 

One way to illustrate this is by using the setTimeout function. In this example, invoking setTimeout, we pass an event handler and a timer in milliseconds. When the timer is up it fires, adding the event handler to the queue. 

setTimeout(function(){
  console.log("This comes first");
}, 0);
console.log("This comes second");
//output in console
// "This comes second"
// "This comes first"

Here we’ve set the timer to zero. But this doesn’t mean “call right away.” It just means “put this in the queue right away.” However the execution of the block of code itself needs to finish, clearing the call stack. Only then is the function from setTimeout given its turn. 

Another common mistake is to think the timer is an exact predictor of when the event handler will fire, but it isn’t necessarily. The event handler still has to wait its turn in the queue. By measuring time, we can see this in action

const timer  = function(){
  let start = Date.now();
  setTimeout(function(){
    let end = Date.now();
    console.log( "Duration: " + (end - start) )
  },1000);
};
timer();
// Console output when invoked several times:
// "Duration: 1007"
// "Duration: 1000"
// "Duration: 1002"
// "Duration: 1004"

The time is set to one second, and it runs pretty close to that. But clearly there is some variance in how quickly the function can be added to the queue and then run each time it’s called. 

Now that we’ve seen some examples of asynchronous calls, we can look at some common asynchronous patterns and constructs. 

Callback Pattern

A callback is simply a function passed into another function that invokes it at some point in the future. 

So in reality, we’ve already seen a bunch of callbacks. 

setTimeout(callback, timer)
Element.addEventListener(event, callback)
Array.map(function(item){...})

Let’s apply this to our bike use case to see how callbacks are implemented. When you shift a bike’s gears, for the most part it works. But there’s still a small chance it can fail. This is an ideal scenario for asynchronous JavaScript. Let's see how it looks with a callback that takes in the data about how the gears are to be shifted, then calls a passed in function when done. 

Bike.prototype.changeGearAsync = function(shiftObject, callback){
  let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
  if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
    callback(new Error("There is a problem"), null);
  } else {
    callback(null, newIndex);
  }
};

The argument callback is actually a function. If there’s an error, we invoke it and set the first argument with whatever error data we want to send back. On success, we null the error argument and pass back the good data. We can then see how our new gear change function is invoked. 

Bike.prototype.changeGear = function(frontOrRear, changeBy) {
  const shiftIndexName = frontOrRear + "GearIndex"
  const that = this;
  //contains state change for making the shift
  let shiftObject = {
    currentIndex: this[shiftIndexName],
    maxIndex: this.transmission[frontOrRear + "GearTeeth"].length,
    changeBy: changeBy
  }
  // invoke async function with anonymous callback
  this.changeGearAsync(shiftObject, function(err, newIndex){
    if (err) {
      console.log("No Change");
    } else {
      that[shiftIndexName] = newIndex;
    }
  });
};

The callback pattern was widely accepted and used extensively, but it has some drawbacks. First, when several callbacks are chained together, they are nested one inside the other. This creates undue complexity, readability problems, and is difficult to reason about when reading someone else’s code. This flaw is known as callback hell. Callbacks also have no implicit error state (like try/catch does). It is up to the developer writing the callback to explicitly look for an error with an if condition. These obstacles led to the creation of promises. 

Arrow Functions

In the previous example, you may have noticed this line.

const that = this;

This is a relic of older JavaScript. We snuck it in just to introduce a newer function syntax: arrow functions. Think back to what happens when a function is invoked. Specifically, that it binds to a new this context. Unlike other variables in scope of the closure of the anonymous function, JavaScript rebinds this when in fact we actually want the this of the containing function. 

A long-standing workaround was to assign this to a new variable (by convention often called self or that) which would then keep the context reference in the closure.

Arrow functions remove this little bit of coding acrobatics by not rebinding this. Arrow function syntax looks like this:

(arg1, arg2) => {...function body...}

It can also be written as an expression:

const doStuff = (arg1, args) => { ... function body ... }

Using an arrow function, we can remove the that = this bit and change the invocation of changeGearAsync to the following.

  // the anonymous function is now an arrow function
this.changeGearAsync(shiftObject, (err, newIndex)=>{
  if (err) {
    console.log("No Change");
  } else {
    // we reference this instead of that
    this[shiftIndexName] = newIndex;
  }
});

Promising Stuff

Promises developed as libraries to handle asynchronous code in a way that made it easier to reason about when your code succeeded or failed. They also contain built-in mechanisms to chain one call after the other. Competing libraries eventually standardized in the browser as the Promise object. Let’s morph bike one more time

Bike.prototype.changeGearAsync = function(shiftObject){
  return new Promise(
    (resolve, reject) => {
      let newIndex = shiftObject.currentIndex + shiftObject.changeBy;
      if (newIndex < 0 || newIndex > shiftObject.maxIndex) {
        reject("New Index is Invalid: " + newIndex);
      } else {
        resolve(newIndex);
      }
    }
  );
};

First, the updated changeGearAsync function takes in the data we pass it and returns a new Promise object. We pass in a single argument: a callback function that itself has two functions passed to it, resolve and reject

When implementing a promise you perform whatever calculations, requests, and so on that you want in the callback function. Once done, if all’s right with the world, you invoke resolve with the data you want to pass back. If you encounter problems you signal that to the function caller by invoking reject along with any relevant errors as the argument. 

Let’s see how we use this now. 

// invoke async function that returns a promise
this.changeGearAsync(shiftObject)
  .then(
    (newIndex) => {
      this[shiftIndexName] = newIndex;
      console.log(this.calculateGearRatio());
    }
  )
  .catch(
    (err) => {console.log("Error: " + err);}
  );

Now we have something a lot easier to reason about. If changeGearAsync works, the then function is invoked with the function passed into its argument. If it does not, catch is invoked. 

If the callback function itself returns an instance of Promise, that’s when things get exciting. You can simply chain those two promise functions together. For instance, if we want to change both the front and rear gears. 

Bike.prototype.changeBothGears = function(frontChange, rearChange) {
  let shiftFront = {
    currentIndex: this.frontGearIndex,
    maxIndex: this.transmission.frontGearTeeth.length - 1,
    changeBy: frontChange
  };
  let shiftRear = {
    currentIndex: this.rearGearIndex,
    maxIndex: this.transmission.rearGearTeeth.length - 1,
    changeBy: rearChange
  };
  this.changeGearAsync(shiftFront)
    .then(
      (newIndex) => {
        this.frontGearIndex = newIndex;
        console.log(this.calculateGearRatio());
        return this.changeGearAsync(shiftRear);
      }
    )
    .then(
      (newIndex) => {
        this.rearGearIndex = newIndex;
        console.log(this.calculateGearRatio());
      }
    )
    .catch(
      (err) => {console.log("Error: " + err);}
    );
  };

The changeBothGears function above shows us chaining two calls to changeGearAsync, each with the object that corresponds to either the front or rear gears. After calling it the first time, we call it again at the end of the first then. Another then can be tacked onto that. Fundamentally, each time a then returns a promise, it can be followed with another then until we’ve exhausted all chained actions.  

Async/Await

Before signing off, it’s worth mentioning some newer additions to the asynchronous arsenal: the async and await operators. These build on promises, allowing them to be used in a way that much more closely resembles synchronous JavaScript.

Lightning Web Components and Asynchronous JavaScript

Lightning Web Components enables the developer to make use of both promise-based asynchronous functionality and the async/await functionality. 

Interacting with Salesforce

There are several features implemented in Lightning Web Components that use asynchronous JavaScript. Most of these revolve around interaction with the server. One example is the way an Apex method can be invoked imperatively in a Lightning Web Component. 

Here is how you would invoke it using a promise-based API: 

import { LightningElement } from 'lwc';
import findContacts from '@salesforce/apex/ContactController.findContacts';
export default class ApexImperativeMethodWithParams extends LightningElement {
    searchKey = '';
    contacts;
    error;
    handleSearch() {
        findContacts({ searchKey: this.searchKey })
        .then(result => {
            this.contacts = result;
            this.error = undefined;
        })
        .catch(error => {
            this.error = error;
            this.contacts = undefined;
        });
    }
}
Note

There are a few things here that are beyond the scope of this module, most notably the use of JavaScript modules in ES6. To learn about these features, this is the perfect time to think about moving on to the next module in this trail, Modern JavaScript Development. But before you do, a few words about this code. 

When we call import findContacts… this is standard module syntax to include the functionality of another module in this component. We surface the findContacts Apex method here as a JS function of the same name. 

When we invoke it in the handleSearch() function, the parameters for the Apex method are passed as a literal object, and then we see the familiar promise-based syntax of a then and a catch function. 

Resources

Teilen Sie Ihr Trailhead-Feedback über die Salesforce-Hilfe.

Wir würden uns sehr freuen, von Ihren Erfahrungen mit Trailhead zu hören: Sie können jetzt jederzeit über die Salesforce-Hilfe auf das neue Feedback-Formular zugreifen.

Weitere Infos Weiter zu "Feedback teilen"