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:
Arrow Functions, introduced in ES6, provide a compact and more readable way to write JavaScript functions. Here's an example:
JavaScript1const 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:
JavaScript1function 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.
HTML, XML1<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. In Arrow Functions, this
retains the value of the enclosing lexical context, whereas in regular functions, this
refers to the object that called the function. Let's see an example to understand this better:
HTML, XML1<button id="btn">Say Hello!</button> 2<script> 3// Add an event listener to the button 4document.getElementById("btn").addEventListener("click", function () { 5 // 'this' refers to the button as expected 6 this.innerHTML = "Hello, I am from Event Listener using a Regular Function"; 7}); 8</script>
In this example, when the button is clicked, the innerHTML
of the button is changed to display the message. Here, this
refers to the button element that triggered the event.
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:
HTML, XML1<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.
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:
HTML, XML1<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.
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:
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:
JavaScript1const 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:
isCoffeeMakerReady
is true
(coffee maker is ready), the Promise is resolved with a value of "Coffee is ready!"
.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.
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:
JavaScript1const 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.
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.
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."catch
block is used to handle any errors that are thrown in the try
block.Looking back at the getCoffee
function:
JavaScript1async 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".
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!