Step by step guide to async programming in Javascript

Step by step guide to async programming in Javascript

Async programming in modern Javascript

Asynchronous programming is a technique that lets your code continue running while it waits for external events, like a user's click or data coming in from the internet. This is different from how code runs in some other languages, where things happen strictly one after the other.

Understanding asynchronous programming can be tricky if you're used to languages like Java or C. That's why it's important to grasp this concept for working with JavaScript, the language that runs in web browsers.

In the past, JavaScript relied on 'callbacks' for asynchronous coding. Callbacks can make your code complex and hard to follow. Fortunately, modern JavaScript (ES6 and beyond) provides improved ways to manage asynchronous operations, making your code cleaner and easier to understand.

Table of contents

  1. Understanding Asynchronous Code
  2. Async and Await in Javascript
  3. Conclusion

Understanding Asynchronous Code

Asynchronous code typically involves three key components:

  • The Trigger: An event or action that occurs at an unpredictable time (e.g., a network request, user input).
  • The Success Handler: Code executed when the trigger resolves successfully.
  • The Error Handler: Code executed if the trigger results in an error.

See an example:


function getUserInfo(username) {
return fetch('https://api.github.com/users/tanvin')
.then(response => response.json());
}

getUserInfo('your-username')
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

Above code uses something called "Promises" which even though not clearly visible in the code itself, they are what actually being returned.

Promise is an object is returned when an async operation like Fetch is performed. A promise can be seen as an object that you can pass around as result of the operation even if the result is actually unknown. At anytime you want to check if the promise has completed (called 'resolved' ) you can query this object.

The code fetch('https://api.github.com/users/tanvin') actually returns a promise. Each promise supports two methods.

  1. Then

This method takes a callback that executes if the async operation executed successfully.

  1. Catch

This method takes a callback that executes if the async operation ended up in error.

The advantage of this approach is that you can write and read code without worrying too much about callbacks.

To give you an example:



function getUserInfo(){
return fetch("'https://api.github.com/users/tanvin");
}

const userInfoPromise = getUserInfo();

// Neither of these methods need to know about current status of the promise.
displayUserName(userInfoPromise);
displayUserRepositories(userInfoPromise);
// Tomorrow if you want add some new bevaviour you can add it here.


Same code when written without promises would look something like this:



function getUserInfo(sucess, failure){
return fetch("https://api.github.com/users/tanvin").then(res=>{
displayUserName(res.json());
displayUserRepositories(res.json());
}).catch(error=>{
displayUserName(error);
displayUserRepositories(error);
});
}
getUserInfo();


The second code is more complex and harder to read and maintain. It also violates separation of concerns as getUserInfo here not only fetches user infor but also determines what to do with that content. Not to mention displayUserName and displayUserRepositories are now more complicated as they have to handle the success and error case differently.

This is even more complicated when you have say multiple operations that need to be chained. In such cases normal callback based approach quickly becomes "callback hell".

On other hand promises provide a very clean way of doing this.



function getUserProfile(userId) {
return fetch("/api/users/"+userId).then(response => response.json());
}

function getUserPosts(userId) {
return fetch("/api/users/"+userId+"/posts").then(response => response.json());
}

getUserProfile(1)
.then(userProfile => getUserPosts(userProfile.id))
.then(posts => console.log(posts))
.catch(error => console.error('Something went wrong:', error));


Async and Await in Javascript

Above promises based approach does make writing and reading code simpler and solves a lot of painpoints of older javascript standards. However, human beings are often not very good at thinking in terms of promises. It is still somewhat confusing.

Human beings think in following manner.

  1. Do something
  2. If that thing is done then do something else.
  3. If there was an error do something entirely different.

This is a very intuitive step by step thinking.

For example say when user visits a page, you want to tell the server the page was visited and ask it to update a page counter. Then update the counter accordingly on the page.



function updateAndDisplayPageCounter() {
updateCounter().then(newCounter=>updateDivShowingCounter(newCounter)).catch(error=>hideDivShowingCounter());
}

updateAndDisplayPageCounter();


Whenever promises are involved, at some point you will have to use the methods "then" and "catch". Wherever they are used, they make it harder to read the code.

Javascript provides a shortcut to write this code.


async function updateAndDisplayPageCounter() {
try{
const newCounter = await updateCounter();
updateDivShowingCounter(newCounter)
}catch(error){
hideDivShowingCounter()
}
}

updateAndDisplayPageCounter();


Above code is exactly same as the code above it. Except that the async and await keywords have simplied how the code is written. It is just syntatic sugar around promises.

updateAndDisplayPageCounter returns a promise just like earlier.

The advantage of this syntax is that it allows you to use a more familiar syntax instead of using "then" and "catch" as long as the method is marked as "async".

Conclusion

JavaScript (ES6 and beyond) offers promises, along with the 'async' and 'await' syntax, to streamline asynchronous code. These features significantly improve readability, maintainability, and the ability to chain operations logically. Understanding these concepts is essential for modern JavaScript developers who want to write clean, efficient asynchronous code.