Getting a Handle on Async JavaScript: From Callbacks to Async/AwaitAsnyc

dev11-07-2024

When working with JavaScript, effectively handling asynchronous operations is crucial. But what exactly makes async programming function seamlessly in JavaScript? There are three core concepts that drive async programming:

  1. Callbacks
  2. Promises
  3. Async/Await

Each concept plays a distinct role in managing asynchronous tasks, and understanding them is key to writing clean, efficient code. Let’s explore each one in detail with examples and see how they work together.


1. Callbacks: The First Step into Async Programming

What is a Callback?

A callback is simply a function passed as an argument to another function. It gets executed once an async operation completes, enabling the program to continue running other tasks while waiting for the async operation to finish.

// Simulate an API call that takes 2 seconds to complete
function getData(callback) {
	setTimeout(() => {
		console.log('Data fetched!');
		callback(); // Call the callback function once data is fetched
	}, 2000);
}

// The function to execute after fetching the data
function processData() {
	console.log('Processing data...');
}

// Calling getData and passing processData as the callback
getData(processData);

In this example, getData() simulates fetching data with a setTimeout. After 2 seconds, it prints “Data fetched!” and then calls processData(). Using callbacks allows us to perform actions after an async operation completes, without halting the program.

The Drawback: Callback Hell

Nesting callbacks inside other callbacks leads to a structure known as “callback hell” or the “pyramid of doom.” It makes the code difficult to read and maintain:

getData(() => {
	processData(() => {
		saveData(() => {
			console.log('All operations completed!');
		});
	});
});

This is where Promises come to the rescue, providing a more structured way of handling async code.


2. Promises: A Cleaner Alternative

What is a Promise?

A promise is an object representing the eventual completion or failure of an asynchronous operation. It has three states:

// Simulate an API call using a Promise
function getData() {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			console.log('Data fetched!');
			resolve('Data'); // Fulfill the promise
		}, 2000);
	});
}

// Consume the promise using .then and .catch
getData()
	.then((data) => {
		console.log('Processing data: ' + data);
	})
	.catch((error) => {
		console.error('An error occurred: ' + error);
	});

Here, getData() returns a Promise that resolves after 2 seconds. Using .then(), the next step is defined: processing the fetched data. If an error occurs, .catch() handles it. Promises provide a much cleaner way to handle async code compared to callbacks, especially for chaining multiple operations.

The Drawback: Promise Chaining

While Promises improve readability, chaining too many .then() methods can still become unwieldy:

Explanation:
Here, getData() returns a Promise that resolves after 2 seconds. Using .then(), the next step is defined: processing the fetched data. If an error occurs, .catch() handles it. Promises provide a much cleaner way to handle async code compared to callbacks, especially for chaining multiple operations.

The Drawback: Promise Chaining
While Promises improve readability, chaining too many .then() methods can still become unwieldy:

To simplify this even further, Async/Await offers a more intuitive syntax.


3. Async/Await: The Modern Approach

What is Async/Await?

Async/Await is syntactic sugar built on top of Promises, making asynchronous code look and behave more like synchronous code. Using async before a function means it always returns a Promise, and await is used to pause execution until the Promise is resolved or rejected.

// Simulate an API call using async/await
async function getData() {
	return new Promise((resolve) => {
		setTimeout(() => {
			console.log('Data fetched!');
			resolve('Data');
		}, 2000);
	});
}

async function process() {
	try {
		const data = await getData(); // Wait for getData to resolve
		console.log('Processing data: ' + data);
	} catch (error) {
		console.error('An error occurred: ' + error);
	}
}

process(); // Run the async function

The process() function is marked as async. Inside it, await pauses execution until getData() resolves, making the code appear synchronous. This approach eliminates the need for chaining .then() methods and improves readability.


Choosing the Right Approach

Understanding and using these concepts effectively is essential for mastering async programming in JavaScript and building robust, scalable applications.

Author's photo

Daniel Fraga

I'm a full-stack web developer focused on building innovative applications in the world of e-commerce. In my free time, I enjoy exploring new possibilities in software development within the e-commerce, finance, or music industries.

See other articles:

undefinedThumbnail

The Playful Job Seeker

Stand out with a killer resume, connect on LinkedIn, and request referrals with personalized messages. It’s about building relationships and playing the numbers game.

Job Hunt09-12-2024

undefinedThumbnail

Streamlined Steps for Developing an App: From Concept to Implementation

Starting a new project is exciting, but it can also feel a bit chaotic. To keep things on track, you need a clear vision of what you want to achieve. In this blog, I share a simple three-step approach to help you plan and tackle your project like a pro. Let’s jump in!

productivity09-10-2024