All around us, we have synchronous and asynchronous activities happening. Can you think of any examples of synchronous activities that you perform on a day-to-day basis?
· Taking a shower
· Sleeping
What about asynchronous activities?
· Cooking
· Watching TV
We've all heard the phrase, 'you cannot serve two masters at a time. This is exactly what synchronous means. You have to do one thing at a time. More often than not, the nature of the operation will not allow you to do anything else.
An easy-to-understand activity that we all do, is taking a shower. Have you ever tried to take a shower and cook at the same time? Don't try it, it won't end well. Have you ever found yourself taking a jog while you're asleep? Probably not.
These are day-to-day examples of synchronous activities that we all perform.
What about asynchronous operations? Watching TV is an asynchronous activity. You can watch TV and eat pizza. You can watch TV and sleep. Cooking is also an asynchronous activity. Just because the rice isn't ready yet, doesn't mean you can't make a phone call, or do dishes. You can do something else as the rice is cooking. This is what we mean by asynchronous tasks. You can do other things while the async task is being fulfilled.
Now let's look at why we need asynchronous code. Couldn't we just write code synchronously and be on our merry way. Well, yes, we can. Let's look at an example.
console.log("Step 1");
for(let i = 0; i <= 9; i++){
console.log(i)
}
console.log("Step 2")
JavaScript code runs from top to bottom. The code is executed line by line, in order.
So in our example the first line will be executed and will print "Step 1" to the console. Then, the for loop will start executing and print numbers 0 through 9. When this operation is complete, the next line will begin to execute and print "Step 2" to the console.
As you can see, the for loop had to be executed in its entirety first, before the next line of code could run. Now look at the same example, but this time, we'll increase the range of numbers.
console.log("Step one")
for(let i = 0; i < 1000000;i++){
console.log(i)
}
console.log("Step 2")
Chances are you'll be waiting for a while. As the loop runs, your browser tab will become unresponsive until the loop is done executing. The main JavaScript process is blocked by the synchronous loop. Synchronous code is also referred to as blocking code.
Your browser will throw a tantrum when a long running process blocks the main thread.
You can see how this can become problematic. So how do we solve this? Cue in asynchronous code, which is also referred to as non-blocking code.
JavaScript provides three main ways of writing non-blocking code.
- Callbacks
- Promises
- Async/Await
Callbacks
A callback is a function that gets passed to another function as a parameter. Let's look at an example of a built-in JavaScript function, that takes in another function as a parameter.
setTimeout(()=>{
console.log("Some asynchronous action")
},1000)
A setTimeout function will wait for a given number of milliseconds, then call its callback function. Callbacks were the de-facto way of handling asynchronous operations in JavaScript for a long time.
Then came ES6, which introduced Promises. We'll look at promises in the next section. The problem with callbacks is as your asynchronous operations increase so does the number of callbacks. This is referred to as callback hell. Let's look at an example.
const stepOne = () => {
console.log("Step 1");
};
const stepTwo = () => {
console.log("Step 2");
};
const stepThree = () => {
console.log("Step 3");
};
const stepFour = () => {
console.log("Step 4");
};
setTimeout(() => {
stepOne();
setTimeout(() => {
stepTwo();
setTimeout(() => {
stepThree();
setTimeout(() => {
stepFour();
}, 2000);
}, 3000);
}, 2000);
}, 1000);
As your asynchronous operations increase, your code becomes harder to read.
Promises were introduced in ES6 as a modern solution for handling asynchronous code in JavaScript
Promises
Promises were introduced in ES6(ECMAScript 2015) as a modern solution to handling asynchronous operations. Promises consist of 3 states:
- Pending
- Fulfilled
- Rejected
When a promise is pending, it has neither been fulfilled nor rejected. As the name suggests, the operation is still pending.
Once the operation is complete, the promise will return one of two states. It will either resolve or reject. If the promise resolves, it means it ran successfully and we can access the value of the promise. If an error occurred, the promise will be rejected with an error. Let's look at an example.
const marvelOrDC = (answer) =>{
// Let's start by capitalizing the user input
answer = answer.toUpperCase()
// This is how we return a promise
return new Promise((resolve,reject)=>{
if(answer === 'MARVEL'){
resolve("You are a Marvel comics fan")
}else if(answer === "DC"){
resolve("You are a DC comics fan")
}
// If the user inputs anything other than Marvel or DC the promise will be rejected
else{
reject("You need to either pick marvel or DC")
}
})
}
marvelOrDC("")
So how do we access the value that is returned by the promise? We call the .then() method on our promise.
marvelOrDC("").then(data=>{
console.log(data)
},(err)=>console.log(err))
The .then() method takes in two parameters. The first is a callback function that gets called when the promise resolves. The second is a callback function that gets called if the promise is rejected.
But there is a cleaner way of handling rejections, using the .catch() method.
marvelOrDC("")
.then(data=>{
console.log(data)
})
.catch(err=>console.log(err))
Now that we have a sense of how to write promises, we can begin to look at async/await, which use promises under the hood.
Async/Await syntax was introduced in ES2017
Async/Await
The async/await syntax is syntactic sugar for writing promises. It's a cleaner less verbose way of handling promises. When handling promises using, async/await, we don't need to chain the .then() or the .catch() methods. Let's rewrite the previous example using async/await to see what I mean.
const marvelOrDC = (answer) =>{
// Let's start by capitalizing the user input
answer = answer.toUpperCase()
// This is how we return a promise
return new Promise((resolve,reject)=>{
if(answer === 'MARVEL'){
resolve("You are a Marvel comics fan")
}else if(answer === "DC"){
resolve("You are a DC comics fan")
}
// If the user inputs anything other than Marvel or DC the promise will be rejected
else{
reject("You need to either pick marvel or DC")
}
})
}
const handlePromise = async(answer) =>{
try {
const result = await marvelOrDC(answer)
console.log(result)
} catch (error) {
console.log(error)
}
}
handlePromise("")
Our handlePromise function is labeled with the keyword async. This allows the function to handle promises as well as facilitate the use of the await keyword, inside the function.
The try/catch block is used to handle the different responses the promise returns. The await keyword will suspend execution until the promise is either resolved, or rejected. If resolved, then result will be logged. If rejected, then the error is logged instead.
Conclusion
We've managed to cover a lot of ground. So kudos to you. This can hopefully serve as a stepping stone for you in your journey to master JavaScript.
Top comments (0)