JavaScript Promises and Async/Await

JavaScript Promises and Async/Await

JavaScript is single-threaded, but many operations like API calls or file reading happen asynchronously. Promises and async/await make handling these asynchronous operations much easier. This tutorial covers how to work with asynchronous JavaScript, from basic promises to modern async/await syntax.

Why Asynchronous JavaScript?

JavaScript runs in a single thread, so blocking operations would freeze the entire application. Asynchronous programming allows the code to continue executing while waiting for operations like network requests, file I/O, or timers to complete.

Understanding Callbacks

Before promises, callbacks were the primary way to handle asynchronous operations.

// Traditional callback approach
function fetchUser(id, callback) {
  setTimeout(() => {
    const user = { id, name: 'John Doe' };
    callback(null, user);
  }, 1000);
}

fetchUser(1, (error, user) => {
  if (error) {
    console.error('Error:', error);
  } else {
    console.log('User:', user);
  }
});

Callbacks can lead to “callback hell” when chaining multiple async operations.

What are Promises?

A Promise represents the eventual completion (or failure) of an asynchronous operation. It can be in one of 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) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('Operation completed!');
    } else {
      reject('Operation failed!');
    }
  }, 1000);
});

// Using the Promise
myPromise
  .then(result => console.log(result))
  .catch(error => console.error(error));

Promise Methods

.then() and .catch()

Chain .then() for success and .catch() for errors.

fetch('https://api.example.com/users')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));

.finally()

Runs regardless of success or failure.

fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Error:', error))
  .finally(() => console.log('Request completed'));

Promise.all()

Wait for multiple promises to complete.

const promise1 = fetch('/api/users');
const promise2 = fetch('/api/posts');
const promise3 = fetch('/api/comments');

Promise.all([promise1, promise2, promise3])
  .then(responses => {
    // All promises fulfilled
    return Promise.all(responses.map(r => r.json()));
  })
  .then(data => console.log('All data:', data))
  .catch(error => console.error('One promise failed:', error));

Promise.race()

Returns the first promise that settles (either fulfills or rejects).

const fastAPI = fetch('/api/fast');
const slowAPI = fetch('/api/slow');

Promise.race([fastAPI, slowAPI])
  .then(response => response.json())
  .then(data => console.log('Fastest response:', data));

Promise.allSettled()

Waits for all promises to settle, regardless of success or failure.

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('Fulfilled:', result.value);
      } else {
        console.log('Rejected:', result.reason);
      }
    });
  });

Async/Await Syntax

Async/await is syntactic sugar built on top of promises, making asynchronous code look synchronous.

Basic async/await

async function getUser() {
  try {
    const response = await fetch('/api/user');
    const user = await response.json();
    console.log('User:', user);
  } catch (error) {
    console.error('Error:', error);
  }
}

getUser();

Async functions always return promises

async function sayHello() {
  return 'Hello!';
}

sayHello().then(message => console.log(message)); // "Hello!"

Error handling with try/catch

async function fetchData() {
  try {
    const response = await fetch('/api/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);
    throw error; // Re-throw to let caller handle
  }
}

Parallel operations with async/await

async function getAllData() {
  try {
    const [users, posts, comments] = await Promise.all([
      fetch('/api/users').then(r => r.json()),
      fetch('/api/posts').then(r => r.json()),
      fetch('/api/comments').then(r => r.json())
    ]);
    
    console.log('Users:', users);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

Converting Callbacks to Promises

Many older APIs use callbacks. You can wrap them in promises.

// Callback-based function
function oldAPI(callback) {
  setTimeout(() => {
    callback(null, 'result');
  }, 1000);
}

// Convert to Promise
function promisifiedAPI() {
  return new Promise((resolve, reject) => {
    oldAPI((error, result) => {
      if (error) reject(error);
      else resolve(result);
    });
  });
}

// Use with async/await
async function useOldAPI() {
  try {
    const result = await promisifiedAPI();
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}

Real-world Examples

API Calls

async function loginUser(credentials) {
  try {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
    });
    
    if (!response.ok) {
      throw new Error('Login failed');
    }
    
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Login error:', error);
    throw error;
  }
}

// Usage
loginUser({ email: '[email protected]', password: 'secret' })
  .then(user => console.log('Logged in:', user))
  .catch(error => console.error('Login failed:', error));

File Operations (Node.js)

const fs = require('fs').promises;

async function readAndProcessFile(filename) {
  try {
    const content = await fs.readFile(filename, 'utf8');
    const processed = content.toUpperCase();
    await fs.writeFile(filename + '.processed', processed);
    console.log('File processed successfully');
  } catch (error) {
    console.error('Error processing file:', error);
  }
}

Sequential vs Parallel Execution

// Sequential (one after another)
async function sequentialTasks() {
  const task1 = await fetch('/api/task1');
  const task2 = await fetch('/api/task2');
  const task3 = await fetch('/api/task3');
  console.log('All tasks completed sequentially');
}

// Parallel (all at once)
async function parallelTasks() {
  const [result1, result2, result3] = await Promise.all([
    fetch('/api/task1'),
    fetch('/api/task2'),
    fetch('/api/task3')
  ]);
  console.log('All tasks completed in parallel');
}

Best Practices

  1. Always use try/catch with async/await: Handle errors properly
  2. Avoid mixing callbacks and promises: Choose one approach consistently
  3. Use Promise.all for parallel operations: When operations don’t depend on each other
  4. Return promises from async functions: Don’t await unnecessarily in return statements
  5. Handle all promise rejections: Use .catch() or try/catch

Common Mistakes

// ❌ Don't do this - swallowing errors
async function badExample() {
  const result = await someAsyncOperation(); // No try/catch
}

// ❌ Don't forget await
async function anotherBadExample() {
  fetch('/api/data'); // Forgot await - promise is ignored
}

// ✅ Do this instead
async function goodExample() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

Summary

Promises and async/await have revolutionized asynchronous JavaScript:

FeaturePromisesAsync/Await
ReadabilityGoodExcellent
Error Handling.catch()try/catch
Chaining.then() chainsSequential code
ParallelPromise.all()Promise.all()
Browser SupportWideModern browsers

Both approaches are valid, but async/await is generally preferred for new code due to its cleaner syntax.


External Resources:

Related Tutorials:

  • Learn about JavaScript functions here to understand how async functions work.
  • Check out ES6 features here for more modern JavaScript syntax.
Last updated on