React Native

Difference Between Callbacks, Promises, and Async/Await in React Native


Introduction

Asynchronous programming is a significant aspect of developing React Native applications. Three most common approaches toward handling asynchronous operations are callbacks, Promises, and Async/Await. While each has their own strengths and weaknesses, when used appropriately, they can boost the performance of your application, as well as make the code more readable.

Understanding Synchronous vs Asynchronous Operations

Before diving into the differences, it's essential to understand the concepts of synchronous and asynchronous operations:

  • Synchronous: A single thread is allocated to a single operation. Each task waits for the previous one to complete before starting.
  • Asynchronous: A single thread handles multiple operations concurrently, allowing tasks to proceed without waiting for others to finish.

Real-World Analogy

Imagine you go to a market for three tasks: repairing your mobile, ironing your clothes, and buying groceries.

  • Synchronous Approach: You wait at the mobile repair shop until your phone is repaired, then move to the laundry to get your clothes ironed, and finally buy groceries. This wastes time because you’re waiting for each task to finish.

  • Asynchronous Approach: You drop your phone in the repair shop, hand over the clothes to be ironed, and then go to the grocery. By the time you finish purchasing, your phone and clothes are ready. This way, no time is wasted by waiting for the other to finish before beginning another.

In programming terms, asynchronous operations allow you to execute tasks independently without blocking the main thread.

1. Callbacks

What Are Callbacks?

A callback is a function that is passed as an argument to another function. Once the asynchronous operation is finished, the callback is called, and the result is delivered. This was one of the earliest approaches to managing asynchronous tasks in JavaScript.

Example in React Native:

function fetchData(callback) { setTimeout(() => { callback("Data fetched"); }, 2000);}fetchData((data) => { console.log(data); // Output: Data fetched});

 

  • Pros: Simple for small-scale tasks. Flexible: Can pass multiple callbacks for different stages (e.g., success, failure).
  • Cons:
  • Callback Hell: Nested callbacks become difficult to read and maintain. Error handling is cumbersome.

  • Use Case: Useful for simple, quick tasks where readability isn't a concern.

2. Promises

What Are Promises?

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. Promises provide a cleaner and more structured way to manage asynchronous flows than callbacks do.

 

How They Work:

A Promise can be in one of three states:

  1. Pending: The operation is still ongoing.
  2. Resolved (Fulfilled): The operation completed successfully.
  3. Rejected: The operation failed.

Here's an example of using JavaScript promises in a real-life scenario: fetching user data and posts from an API.

You want to display a user's profile along with their latest posts. First, you fetch the user data, and after that, you fetch their posts based on the user ID. Promises ensure these asynchronous tasks are handled efficiently.

 

// Simulate an API call to fetch user datafunction fetchUser(userId) { return new Promise((resolve, reject) => { console.log("Fetching user data...."); setTimeout(() => { if (userId) { resolve({ id: userId, name: "John Doe" }); } else { reject("User ID is required!"); } }, 1000); // Simulating a network delay });}// Simulate an API call to fetch user postsfunction fetchPosts(userId) { return new Promise((resolve, reject) => { console.log("Fetching user posts..."); setTimeout(() => { if (userId) { resolve([ { id: 1, title: "My First Post" }, { id: 2, title: "Learning Promises" }, ]); } else { reject("Failed to fetch posts. User ID is missing."); } }, 1500); // Simulating a network delay });}// Usageconst userId = 123;fetchUser(userId) .then((user) => { console.log("User fetched:", user); return fetchPosts(user.id); // Chain the promise to fetch posts }) .then((posts) => { console.log("User posts fetched:", posts); }) .catch((error) => { console.error("Error:", error); }) .finally(() => { console.log("All tasks are completed."); });

Output:

Fetching user data...User fetched: { id: 123, name: 'John Doe' }Fetching user posts...User posts fetched: [ { id: 1, title: 'My First Post' }, { id: 2, title: 'Learning Promises' }]All tasks are completed.

Key Points:

  1. Promises handle asynchronous operations, like API calls, without blocking the main thread.
  2. Chaining (.then) ensures tasks are performed sequentially (e.g., fetching posts only after user data is fetched).
  3. Error Handling (.catch) manages errors gracefully, such as missing or invalid data.
  4. Final Task (.finally) runs regardless of success or failure, useful for cleanup or final actions.

Pros:

  1. Avoids callback hell by using .then() for chaining.
  2. Built-in error handling with .catch().
  3. Easy to compose multiple asynchronous tasks.

Cons:

  1. If not structured well, promise chains can still become hard to follow.
  2. Errors in one part of the chain can affect subsequent operations if not handled properly.

Use Case:

Promises are ideal when you need to chain multiple asynchronous operations, such as fetching data from an API and then processing or displaying it.

3. Async/Await

What Is Async/Await?

Async/Await is syntactic sugar built over Promises, allowing you to write asynchronous code that looks just like synchronous one, making code easier to read and debug; this is now the most up-to-date method of handling anything asynchronous in the JavaScript world.

 

How It Works:

Functions declared with async automatically return a Promise.

The await keyword pauses the execution of the function until the Promise is resolved or rejected.

We’ll take above example with async/await 

// Simulate an API call to fetch user datafunction fetchUser(userId) { return new Promise((resolve, reject) => { console.log("Fetching user data..."); setTimeout(() => { if (userId) { resolve({ id: userId, name: "John Doe" }); } else { reject("User ID is required!"); } }, 1000); // Simulating a network delay });}// Simulate an API call to fetch user postsfunction fetchPosts(userId) { return new Promise((resolve, reject) => { console.log("Fetching user posts..."); setTimeout(() => { if (userId) { resolve([ { id: 1, title: "My First Post" }, { id: 2, title: "Learning Promises" }, ]); } else { reject("Failed to fetch posts. User ID is missing."); } }, 1500); // Simulating a network delay });}// Async function to handle the flowasync function displayUserAndPosts(userId) { try { console.log("Starting task..."); const user = await fetchUser(userId); // Wait for user data console.log("User fetched:", user); const posts = await fetchPosts(user.id); // Wait for posts data console.log("User posts fetched:", posts); } catch (error) { console.error("Error:", error); // Catch and log errors } finally { console.log("All tasks are completed."); }}// Call the async functiondisplayUserAndPosts(123);

 

Output:

Starting task...Fetching user data...User fetched: { id: 123, name: 'John Doe' }Fetching user posts...User posts fetched: [ { id: 1, title: 'My First Post' }, { id: 2, title: 'Learning Promises' }]All tasks are completed. 

Pros:

  1. Code is more readable and easier to understand.
  2. Handles errors with try/catch, making debugging simpler.
  3. Allows asynchronous code to flow sequentially, improving clarity.

Cons:

  1. Requires the function to be declared with async.
  2. Debugging can be challenging when multiple await calls are used in parallel.

Use Case:

Async/Await is perfect for managing complex workflows where operations depend on the results of previous tasks.

Key Differences

Callbacks:

  • Readability: Poor with nesting
  • Error Handling: Requires manual handling
  • Debugging: Difficult
  • Chaining: Tedious
  • Code Complexity: High with nested calls
  • Execution Flow: Hard to follow

Promises:

  • Readability: Better than Callbacks
  • Error Handling: .catch()
  • Debugging: Easier
  • Chaining: Possible with .then()
  • Code Complexity: Moderate
  • Execution Flow: Easy to follow

Async/Await:

  • Readability: Excellent
  • Error Handling: try/catch
  • Debugging: Simplified
  • Chaining: Sequential with await
  • Code Complexity: Low
  • Execution Flow: Synchronous-like

 

When to Use What in React Native

  • Callbacks: Use for lightweight, straightforward tasks or when interacting with legacy APIs.
  • Promises: Well suited for asynchronous code in multiple steps..
  • Async/Await: Best suited for modern React Native development, especially when complex logic is involved and error handling and sequential flows are required.

Conclusion

Understanding and applying Callbacks, Promises, and Async/Await effectively is crucial for React Native developers. Each approach has its place in handling asynchronous operations:

  • Callbacks are great for simple tasks but can lead to callback hell for complex workflows.
  • Promises provide a cleaner way to manage asynchronous tasks, especially when chaining multiple operations.
  • Async/Await offers the most readable and maintainable solution for modern JavaScript development.

Choosing the right approach depends on your use case, but Async/Await is often the best choice for its simplicity and ease of debugging.

Ready to transform your business with our technology solutions? Contact Us today to Leverage Our React Native Expertise.

0

React Native

Related Center Of Excellence