Lesson 3

Mastering Asynchronous Programming in JavaScript

Introduction to Asynchronous JavaScript

In our exciting journey of learning JavaScript, we have encountered various functions and methods used to create dynamic web pages. By now, you might understand how HTML is used for webpage layout, and how JavaScript makes it more interactive by manipulating the DOM. In this chapter, we are going to delve deeper and explore Asynchronous JavaScript, which makes our web pages faster and more efficient.

But why do we need Asynchronous JavaScript in the first place? Imagine you are on a shopping website, and you click on a button to view a product. If the JavaScript code for loading the product details is synchronous, the entire web page would freeze until the product details are loaded. This is not a great user experience, is it? To solve this problem and make our web pages more interactive, we use several features in JavaScript like Arrow Functions, Callbacks, the setTimeout method, Promises, and Async/Await. Let's break down each one of these:

Introduction to Arrow Functions

Arrow Functions, introduced in ES6, provide a compact and more readable way to write JavaScript functions. Here's an example:

1const functionName = (param1, param2, ..., paramN) => { 2 // code 3}

In the context of Arrow Functions, the const keyword is used to declare the function name, followed by the list of parameters in parentheses ( ), the arrow symbol =>, and then the function code inside { } braces.

But how does the same thing look if we were to use regular function syntax? Here's how:

1function functionName(param1, param2, ..., paramN) { 2 // code 3}

As you can see, standard function syntax involves using the function keyword, the function name, and the parameters in the parentheses ( ), and the function code inside { } braces.

Let's look at the example below where we use an Arrow Function as an event handler for a button.

1<button onclick="showHello()">Say Hello!</button> 2 3<p id="demo"></p> 4<script> 5// Declare 'showHello' as an Arrow Function 6const showHello = () => { 7 // Change the text inside the paragraph when the function is called 8 document.getElementById("demo").innerHTML = "Hello, I am an Arrow Function!"; 9} 10</script>

One key difference between Arrow Functions and regular functions is the this keyword. Inside Arrow Functions, it is always bound to the value of this in the closest non-arrow parent function.

This feature of this in Arrow Functions is extremely helpful when dealing with event handlers. If we attempted to use a traditional function as an event handler in the following example, it wouldn't work as expected, because the this keyword wouldn't refer to the button element:

1<button id="btn">Say Hello!</button> 2<script> 3// Add an event listener to the button 4document.getElementById("btn").addEventListener("click", () => { 5 // 'this' refers to the button as expected 6 this.innerHTML = "Hello, I am from Event Listener using Arrow Function"; 7}); 8</script>

In this example, the this keyword correctly refers to the button element, even inside the event handler. This is one of the reasons Arrow Functions are often used with event handlers in JavaScript.

Into The World of Callbacks

JavaScript heavily uses callbacks — functions passed into others and invoked later to handle asynchronous operations. But what does that mean? Let's say you are preparing to cook dinner. While the water is being boiled for pasta, you chop the vegetables for the salad simultaneously. In this case, chopping the vegetables is similar to a "callback" function, which you do while waiting for the pasta water to boil. In other words, a "callback" allows us not to waste time while waiting for an operation (like boiling water) to complete.

Let's see how we can use callbacks in JavaScript:

1<button onclick="greetUser(showGreeting)">Say Hi!</button> 2<p id="demo"></p> 3<script> 4function showGreeting(name) { 5 // The 'callback' we want to execute after the name is entered 6 document.getElementById("demo").innerHTML = "Hi " + name; 7} 8function greetUser(callback) { 9 // The 'prompt' function displays a dialog box that prompts the user for input. 10 // It takes two arguments: a message to display, and a default value for the input field. 11 let name = prompt("Please enter your name", "Harry Potter"); 12 callback(name); 13} 14</script>

In this example, when the button is clicked, the greetUser function is called, and a prompt box asks you to enter a name. Once you enter a name, greetUser uses the showGreeting function as a callback with name as its argument, and the greeting message is displayed on the web page. This shows how callback functions can make our JavaScript interactive and efficient.

Knowing the `setTimeout` Method

setTimeout() is a built-in JavaScript function that executes a piece of code after waiting a specific amount of time.

Let's illustrate this with a simple script that hides a button after 3 seconds:

1<button id="btn">Disappear after 3 seconds</button> 2<script> 3const btn = document.getElementById("btn"); 4setTimeout(() => { 5 // Hide the button after 3 seconds 6 btn.style.display = "none"; 7}, 3000); 8</script>

In the example above, the setTimeout method waits for 3 seconds (3,000 milliseconds) and then hides the button. During this delay, the JavaScript engine can do other things. It doesn’t just sit there and waste time, hence, it’s used to introduce asynchronous execution in JavaScript.

An Encounter with Promises

Promises in JavaScript represent the eventual result of an asynchronous operation, thus providing a method for handling the result or error that the operation can return. Creating promises in JavaScript helps us in making the async code cleaner and more readable.

A Promise can be in one of the following states:

  1. pending: The Promise's outcome hasn't yet been determined.
  2. fulfilled: The operation completed successfully.
  3. rejected: The operation failed.

To create a promise, you use the Promise constructor that accepts a handler that offers two functions as parameters: resolve and reject. The resolve function changes the state of the Promise from pending to fulfilled, while the reject function changes the state of the Promise from pending to rejected. Take a look at the example below:

1const isCoffeeMakerReady = true; 2let orderCoffee = () => { 3 return new Promise((resolve, reject) => { 4 if(isCoffeeMakerReady) { 5 resolve("Coffee is ready!"); 6 } else { 7 reject("Coffee maker is not ready."); 8 } 9 }); 10} 11 12orderCoffee().then(message => { 13 console.log(message); // logs: Coffee is ready! 14}).catch(message => { 15 console.log(message); // We would see the message 'Coffee maker is not ready.' if 'isCoffeeMakerReady' boolean was false, since in that case the reject function would be called 16});

This JavaScript example demonstrates the use of a Promise for an asynchronous operation — making coffee.

We have a constant, isCoffeeMakerReady, which represents the state of the coffee maker. It's currently set to true, that means the coffee maker is ready to make coffee.

The orderCoffee function returns a Promise. Inside that Promise, there's a conditional that checks the coffee maker's status:

  • If isCoffeeMakerReady is true (coffee maker is ready), the Promise is resolved with a value of "Coffee is ready!".
  • If isCoffeeMakerReady is false (coffee maker is not ready), the Promise is rejected with a reason, "Coffee maker is not ready.".

When you call the orderCoffee() function, it returns a Promise because the function is wrapped in a Promise interface. So, you can treat it like a Promise and call then to handle resolution or catch to handle rejection.

Here's how it works:

  • orderCoffee().then(message => {...}) is executed when the Promise is resolved. The message argument is the value that was passed to resolve in the Promise. It logs the message: "Coffee is ready!" to the console.

  • orderCoffee().catch(message => {...}) is executed when the Promise is rejected. The message argument is the reason that was passed to reject in the Promise. It logs the message: "Coffee maker is not ready." to the console.

Currently, since isCoffeeMakerReady is true, we receive "Coffee is ready!" in console. If isCoffeeMakerReady was false, we'd see "Coffee maker is not ready." instead.

Apprehending Async/Await

Async/Await is a modern approach to deal with asynchronous JavaScript code in a more efficient and simpler way. It makes asynchronous code look and behave a little more like synchronous code.

Take a look at the example below:

1const isCoffeeMakerReady = true; 2 3let orderCoffee = () => { 4 return new Promise((resolve, reject) => { 5 if(isCoffeeMakerReady) { 6 resolve("Coffee is ready!"); 7 } else { 8 reject("Coffee maker is not ready :("); 9 } 10 }); 11}; 12 13async function getCoffee() { 14 try { 15 let message = await orderCoffee(); 16 console.log(message); // logs: Coffee is ready! 17 } catch (message) { 18 console.log(message); // logs: Coffee maker is not ready :( 19 } 20} 21 22getCoffee();

The await keyword is used in an async function to pause its execution until a Promise is either fulfilled (resolved) or rejected. The resolved value of the Promise is then assigned to the variable to the left of the await keyword.

In this example, async and await are used within the getCoffee function to handle the asynchronous orderCoffee function which returns a Promise:

  • When getCoffee is invoked, it hits the line let message = await orderCoffee();. Here, the await keyword causes the function to pause until the Promise returned by orderCoffee is settled.

  • If the Promise is resolved, i.e., isCoffeeMakerReady is true, then "Coffee is ready!" is returned and assigned to the variable message.

  • If the Promise is rejected, i.e., isCoffeeMakerReady is false, then "Coffee maker is not ready :(" is thrown as an error that we can catch with a catch block.

  • message is then logged to the console. Depending on the value of isCoffeeMakerReady, it will either be "Coffee is ready!" or an error message.

There's an important point to note here: all the variables and the results of the operations following the await operation will have to wait until the Promise is settled. They don't get computed concurrently. Hence, it's important to consider the implications of using await if the operations are independent and can benefit from being computed concurrently.

Understanding the Try-Catch Block in Async/Await

When working with async/await in JavaScript, error handling becomes crucial to manage asynchronous code effectively. This is where the try-catch block comes into play.

  • The try block is used to wrap code that might throw an error. It's a way of saying, "I think this operation might fail, and I'm prepared to handle it."
  • The catch block is used to handle any errors that are thrown in the try block.
Example in Context

Looking back at the getCoffee function:

1async function getCoffee() { 2 try { 3 let message = await orderCoffee(); 4 console.log(message); // Executed if orderCoffee fulfills 5 } catch (message) { 6 console.log(message); // Executed if orderCoffee rejects 7 } 8}
  • In this function, await orderCoffee() is inside the try block. If orderCoffee resolves, message will log "Coffee is ready!". If orderCoffee rejects, the control moves to the catch block.

  • The catch block logs the rejection reason, which in this case is "Coffee maker is not ready".

Wrapping Up

And just like that, we have come to the end of a riveting session where we unraveled the world of Asynchronous JavaScript. We've learned about Arrow Functions, Callbacks, the setTimeout method, Promises, and Async/Await, and saw how they can be used to manage asynchronous JavaScript operations effectively, making our web pages more user-friendly and responsive.

In our upcoming practice sessions, you'll gain a better hands-on understanding of these concepts when we dive into coding. Remember, the best way to get comfortable with these daunting terms is through practice, practice, and more practice. So, buckle up and get ready to dive deep into the intriguing world of Asynchronous JavaScript!

Enjoy this lesson? Now it's time to practice with Cosmo!

Practice is how you turn knowledge into actual skills.