Photo by Shahadat Rahman on Unsplash
Handling Asynchronous Operations in JavaScript
The old, new and efficient way.
Synchronous and Asynchronous Operations on JavaScript.
JavaScript is a single-threaded programming language which means it has only one call stack and one memory leap and performs one single operation at a time. In other words, JavaScript is synchronous in nature. Synchronous operations imply that the operations occur in sequence i.e every statement of the code gets executed one after the other. To get more understanding of how JavaScript work behind the scene, check this article where I explained how JavaScript works internally.
Now, what are Asynchronous operations?
Here, let's get our friends from MDN to define this for us
Asynchronous programming is a technique that enables your program to start a potentially long-running task, and then rather than having to wait until that task has finished, be able to continue to be responsive to other events while the task runs. Once the task is completed, your program is presented with the result.
This can be understood as running some program in the background without blocking the thread. Since JavaScript is single-threaded, blocking the thread means that nothing will happen and no operation will run until the long-running task is completed and the thread is unblocked. Examples of asynchronous operations in JavaScript are:
- making HTTP requests with
fetch()
- Using JavaScript
setTimeout()
- accessing the user's camera or microphone with
getUserMedia()
Old way of handling Asynchronous Operations in JavaScript.
At some points, you might have heard of callbacks in JavaScript. A JavaScript callback is a function that is to be executed after another function has finished execution. A more formal definition would be - Any function that is passed as an argument to another function so that it can be executed in that other function is called a callback function. An example of this can be seen in the snippet below:
function successCallback() {
console.log("Hello there, I am back with the data successfully")
}
function failureCallback() {
console.log("Oh, I couldn't get the data, something happened on the way")
}
getMeSomeDataFromTheNet(successCallback, failureCallback);
The irony of this method of handling asynchronous operations is that in no time, the whole code can turn messy, and a popular name for this mess is what is called a Callback HellβοΈ. An example of that is thisπ
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
The code becomes hard to read and maintain.
Let's see how to escape this callback hell and write more clean, readable, and maintainable code.π
Promises, an escape from the callback hell.
Promises specifications were added to ES6 in 2012 and the main reason for that was to handle asynchronous operations in JavaScript. Earlier, we discussed how we can easily run into a problem while handling asynchronous operations in JavaScript, to avoid this problem, the JavaScript promise is our way out.
Promise has three states. They are:
- pending: Promise is in its initial state, neither fulfilled nor rejected.
- fulfilled: the operation was completed successfully.
- rejected: Action/operation related to the promise failed for a reason(i.e error).
To handle promises, an inbuilt method then
and catch
are used in determining what happens after the promise has been resolved i.e fulfilled or rejected respectively. The then
and catch
methods also return a Promise. This means that promises can be chained! And that's an interesting feature of Promises. Let's see an example of this:
fetch(`${baseUrl}/point-to-some-api-online`)
.then((res) => {
console.log("If done, I'd move to the next one or fail and exit", res)
})
.then((res) => {
console.log("I'd move on when I'm done. or fail and exit", res)
})
.then(() => {
console.log("I'm at the final destination. I pass or fail.")
})
.catch((err) => console.log("Something went wrong in any of the above, take a look", err));
It can be noticed that a single catch
method has been added to handle errors in any of the chained promises above. The code above is more readable and maintainable than its equivalent as we saw earlier.
Another example is this, where we create our Promise using the Promise
constructor:
const myPromise = new Promise((resolve, reject) => {
const x = "javascript is fun";
const y = "javascript is !hard"
if(x === y) {
resolve();
} else {
reject();
}
});
myPromise
.then((res) => res.json)
.then((data) => console.log(data))
.then(doSomething)
.catch(handleAnyRegected)
But this can be better! Let see ππ
Async/Await, the syntactic sugar on top of JavaScript Promises.
Async/await allows you to write asynchronous code synchronously. And it's lots easier to read. To handle errors in an async/await function, the operations in the function are wrapped in a try/catch block. The code in the try block runs and if failed, the codes in the catch block are executed and the error can be effectively handled. Let's see an example here:
const fetchUser = async () => {
try {
const res = await fetch(`https://randomuser.me/api/`);
console.log(res);
const data = await res.json();
console.log(data);
} catch (err) {
console.error("An Error occured", err);
throw err;
}
};
fetchUser();
Let's step through this code synchronously to understand what is happening:
async
keyword: tells the JavaScript engine running the code that the code block is a Promise and its pending resolution and it can move to the next line after the block. This means that if we have a console.log after the async function block, it will be executed/logged before any of the code in the async block gets executed.try block: this mechanism is necessary for error handling in our async function. If the code in the try block fails, the code in the catch block is executed.
await
keyword: this implies that the code execution is stopped temporarily until the Promise is resolved. In this case, until some data is returned from thefetch
, the process is blocked. Don't be confused here as the blocking here has nothing to do with the main thread. The blocking here happens in the local execution context of this function. If resolved successfully, the code on the next line gets executed, and the result is shown in the console. The same thing happens for the secondawait
. If it fails, the catch block is executed and thethrow err
exits the code execution.
Conclusion.
Personally, I'd recommend using async/await unless there is a need for the chaining with then
method. To read more, check the links below:
MDN documentation on Promises: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Asynchronous JavaScript: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous
If you find this useful consider leaving a ππ» and share. You can also connect with me on Twitter @abdulsalam_mn.
Thanks for reading.