Promises and async/await

Acadestine

Promises and async/await

Promises and async/await are essential for handling asynchronous operations in JavaScript. Whether you're fetching data from an API, reading files, or working with timers, these patterns help you write cleaner, more readable code. In React, they are used for data fetching, handling side effects, and working with hooks like useEffect.


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

  • Pending – initial state, neither fulfilled nor rejected.
  • Fulfilled – operation completed successfully.
  • Rejected – operation failed.

Creating a Promise

const myPromise = new Promise((resolve, reject) => {
  // Do some async work
  const success = true;
  if (success) {
    resolve('Operation successful!');
  } else {
    reject('Operation failed!');
  }
});

Consuming a Promise – .then() and .catch()

myPromise
  .then(result => console.log(result)) // 'Operation successful!'
  .catch(error => console.error(error))
  .finally(() => console.log('Done')); // Always runs

Key Insight: Promises allow you to write asynchronous code that doesn't rely on nested callbacks (callback hell).


You can chain .then() calls to perform sequential asynchronous operations.

js fetchUserData(userId) .then(user => fetchUserPosts(user.id)) .then(posts => console.log(posts)) .catch(error => console.error('Something went wrong:', error));

Each .then() returns a new promise, allowing you to transform data along the way.


Promise.all()

Waits for all promises to resolve. If any rejects, it rejects immediately.

``` js const promise1 = fetch('/api/user1'); const promise2 = fetch('/api/user2'); const promise3 = fetch('/api/user3');

Promise.all([promise1, promise2, promise3]) .then(responses => console.log(responses)) .catch(error => console.error('One request failed:', error)); ```

Promise.race()

Returns the result of the first promise that settles (resolves or rejects).

js Promise.race([ fetch('/api/data'), new Promise((_, reject) => setTimeout(() => reject('Timeout'), 5000)) ]) .then(data => console.log(data)) .catch(error => console.error(error));

React Use Case

Promise.all is useful for fetching data from multiple endpoints simultaneously before rendering a page.


async/await is a cleaner syntax built on top of Promises. It makes asynchronous code look synchronous.

async Function

An async function always returns a Promise.

async function getUserData(userId) {
  return `User ${userId}`;
}
// The above is equivalent to:
function getUserData(userId) {
  return Promise.resolve(`User ${userId}`);
}

await Keyword

await pauses the execution of the async function until the Promise is resolved.

async function getPost() {
  try {
    const response = await fetch('/api/post');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching post:', error);
  }
}

Important: await can only be used inside async functions (or at the top level in modern modules).


Use try/catch to handle errors when using await.

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error.message);
    // Return a fallback value or re-throw
    return null;
  }
}

You can also chain .catch() if you prefer:

async function fetchData() {
  const response = await fetch('https://api.example.com/data').catch(err => {
    console.error('Network error:', err);
    return null;
  });
  if (!response) return;
  // process response...
}

In React, useEffect is the most common place for data fetching with async/await.

Basic Pattern

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchUsers() {
      try {
        const response = await fetch('https://api.example.com/users');
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUsers(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    fetchUsers();
  }, []); // Empty dependency array = run once

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

Using Custom Hooks

You can extract the fetch logic into a custom hook for reusability.

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();
    async function fetchData() {
      try {
        const response = await fetch(url, { signal: abortController.signal });
        if (!response.ok) throw new Error('Fetch failed');
        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') setError(err.message);
      } finally {
        setLoading(false);
      }
    }
    fetchData();
    return () => abortController.abort(); // Cleanup
  }, [url]);

  return { data, loading, error };
}

Best Practice: Always handle cleanup (aborting fetch) in useEffect to avoid memory leaks.

Event Handlers with async/await

function SubmitButton() {
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      if (response.ok) {
        // Handle success
      }
    } catch (error) {
      // Handle error
    }
  };
  return <button onClick={handleSubmit}>Submit</button>;
}

  • Promises – represent asynchronous operations with .then(), .catch(), and .finally().
  • Promise.all() – waits for all promises to resolve; Promise.race() – resolves with the first settled promise.
  • async/await – syntactic sugar that makes Promises easier to read and write.
  • Error handling – use try/catch with async/await, or .catch() with Promises.
  • In React, use useEffect for data fetching, handle cleanup, and manage loading/error states.
Key Takeaway

"Promises and async/await turn asynchronous chaos into readable, maintainable code. They are essential for any modern React application that talks to a server."

Happy coding!