PROGRAMMING-CONCEPTS

Promise and Async/Await: Definition, Purpose, and Examples

A Promise represents a value that will be available in the future. It acts as a placeholder for the result of an asynchronous operation — something that takes time, like fetching data from an API, reading a file, or waiting for a timer.

Async/await is a more modern syntax built on top of Promises that lets you write asynchronous code in a natural, step-by-step style without getting lost in callback chains. Both are essential to modern JavaScript and TypeScript, especially in browser applications and React.


Why Asynchronous Code Exists in the First Place

Many tasks don’t finish immediately.

A program may need to:

  • Request data from a server
  • Delay execution for a second
  • Read a file or compute something expensive
  • Wait for user interaction

Instead of freezing the entire program until the task finishes, asynchronous programming allows the rest of the code to continue running. Promises and async/await give developers a structured way to work with these delayed results.


What a Promise Is

A Promise is an object that represents a future value. It can be in one of three states:

  • Pending — still waiting
  • Fulfilled — completed successfully
  • Rejected — failed with an error

Once fulfilled or rejected, the Promise becomes settled, and its result can be used.

JavaScript example:

const dataPromise = fetch("/api/user");

fetch() immediately returns a Promise. The network request happens in the background; your code stays responsive.

To access the result:

dataPromise.then(response => response.json())

The then method provides a callback that runs when the Promise resolves.

This pattern is the foundation for modern web APIs.


Promise Chains

If several asynchronous steps depend on each other, Promises can chain operations:

fetch("/api/user")
  .then(res => res.json())
  .then(user => fetch(`/api/orders/${user.id}`))
  .then(res => res.json())

Each .then(...) waits for the previous step to finish.

This chaining avoids callback nesting, but long chains can still become unwieldy — which is why async/await was introduced.


Async/Await: A Better Way to Work with Promises

Async/await sits on top of Promises but makes asynchronous code look synchronous.

async function loadUser() {
  const res = await fetch("/api/user");
  const user = await res.json();
  return user;
}

Each await pauses the function until the Promise resolves, without blocking the entire program.

This style improves readability and makes complex asynchronous flows easier to understand.

Using this function:

loadUser().then(user => console.log(user));

Even though loadUser looks synchronous, it still returns a Promise. This keeps the entire system consistent.


Handling Errors

Promises use .catch() to handle failures:

fetch("/api/user")
  .then(res => res.json())
  .catch(err => console.error("Error:", err));

Async/await uses try / catch:

async function getUser() {
  try {
    const res = await fetch("/api/user");
    return await res.json();
  } catch (err) {
    console.error("Failed:", err);
  }
}

This makes error-handling feel closer to normal control flow.


Parallel vs Sequential Execution

Sometimes multiple asynchronous tasks can run at the same time.

Parallel execution:

const [user, orders] = await Promise.all([
  fetch("/api/user").then(r => r.json()),
  fetch("/api/orders").then(r => r.json())
]);

This waits for both operations to finish and is much faster than running them sequentially.

Sequential execution:

const user = await fetch("/api/user").then(r => r.json());
const orders = await fetch(`/api/orders/${user.id}`).then(r => r.json());

This runs one step after the other — necessary when the second depends on the first.

Understanding this difference greatly impacts performance.


TypeScript and Promises

TypeScript improves safety by giving async code explicit return types.

async function loadProfile(): Promise<User> {
  const res = await fetch("/profile");
  return res.json();
}

This guarantees the function returns a Promise resolving to User.

It prevents mistakes where a developer returns the wrong type or forgets to handle null/undefined scenarios.


React and Promises

React doesn’t allow async functions directly inside components, but async/await is common in effects.

useEffect(() => {
  async function load() {
    const res = await fetch("/api/tasks");
    setTasks(await res.json());
  }
  load();
}, []);

This pattern:

  • fetches data
  • updates state
  • triggers automatic re-rendering

React also uses Promises internally in features like Suspense, which prepares the framework for more parallel UI rendering.


Python’s async/await (for comparison)

Python 3.7+ includes its own async model inspired by Promises.

import asyncio

async def load_user():
    return {"name": "Alice"}

async def main():
    user = await load_user()
    print(user)

asyncio.run(main())

Python’s version uses coroutines, but the core idea is the same:

async defines an asynchronous function; await pauses it until a future value is ready.

This parallel helps learners transfer understanding across languages.


Swift’s Concurrency Model (for comparison)

Swift also uses async/await:

func fetchUser() async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

Swift’s design strongly resembles JavaScript’s async/await, reinforcing the consistency of modern asynchronous patterns.


Real-World Example: Loading Data for a Dashboard

A dashboard may need several pieces of data: user info, tasks, notifications.

Using async/await:

async function loadDashboard() {
  const [user, tasks, notifications] = await Promise.all([
    fetch("/api/user").then(r => r.json()),
    fetch("/api/tasks").then(r => r.json()),
    fetch("/api/notifications").then(r => r.json())
  ]);

  return { user, tasks, notifications };
}

Each API call runs in parallel, and the result is assembled cleanly.

This approach is both fast and readable.


Real-World Example: Graceful Error Handling

When one API might fail while others succeed:

async function loadSafely(url) {
  try {
    const res = await fetch(url);
    return await res.json();
  } catch {
    return null;
  }
}

const user = await loadSafely("/api/user");
const settings = await loadSafely("/api/settings");

This makes the application resilient to partial failures without crashing.


Common Mistakes

Because asynchronous code behaves differently than synchronous code, beginners often fall into predictable traps.

Forgetting to return Promises

async function load() {
  fetch("/api/data"); // does nothing useful by itself
}

If you don’t await or return it, nothing waits for the result.

Mixing callback-style and Promise-style code

This leads to unpredictable behavior.

Using async/await inside forEach

forEach does not handle asynchronous control flow the way people expect.

Treating async functions as synchronous

For example:

const result = asyncFunction(); // not the value — this is a Promise

Understanding that async functions always return Promises is essential.


Best Practices

  • Prefer async/await over long Promise chains
  • Use Promise.all when tasks can run in parallel
  • Wrap asynchronous code in try / catch
  • Never block the main thread
  • Keep asynchronous steps small and composable
  • Use TypeScript to catch incorrect Promise usage early
  • In React, avoid making useEffect itself async — define an inner async function

Learning these habits early prevents a large category of real-world bugs.


Summary

Promises represent future values, while async/await provides a clearer way to work with those values without getting trapped in nested callbacks. They enable smooth handling of tasks that take time, such as API requests, timers, file reads, and user interactions. Today, every major ecosystem — JavaScript, TypeScript, React, Python, Swift — relies on these asynchronous tools to build responsive, reliable applications.

Learn to Code for Free
Start learning now
button icon
To advance beyond this tutorial and learn to code by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

Reach your coding goals faster