TL;DR: A Promise is JavaScript's way of handling operations that take time — API calls, database queries, file reads. It represents a value that does not exist yet but will eventually. A Promise is either pending (waiting), fulfilled (succeeded with a value), or rejected (failed with an error). The async/await syntax makes Promises readable: await pauses until the Promise resolves, and try/catch handles errors.

Why AI Coders Need to Know This

Every non-trivial AI-generated JavaScript application uses Promises. The moment your app does anything that involves waiting — fetching data from an API, reading from a database, loading a file, sending an email — Promises are involved. If you count the number of await keywords in a typical AI-generated Next.js app, it is often dozens per page.

For vibe coders, Promise-related errors are the most confusing category of bugs. Here is what they look like:

  • UnhandledPromiseRejection — the app crashes with a cryptic message
  • A variable is undefined even though the data "should" be there
  • A function returns Promise { <pending> } instead of the actual value
  • API data loads but the page renders before the data arrives
  • Multiple API calls happen in sequence when they could run in parallel

Every one of these is a Promise issue. AI tools generate the code, and when it works, you never think about Promises. When it breaks, you cannot fix it without understanding how Promises work.

The good news: Promises follow a small set of rules. Once you learn them, async code goes from mysterious to mechanical.

The Core Idea: Values That Take Time

In regular JavaScript, values are instant:

const name = 'Chuck';        // Instant — the value is right here
const total = 10 + 20;       // Instant — calculated immediately
console.log(name);            // 'Chuck' — available now

But some values take time to arrive:

const response = fetch('https://api.example.com/data');
// response is NOT the data yet — it is a Promise
// The data is still traveling over the network

A Promise is JavaScript's way of saying: "I do not have the value yet, but I will. Wait for me." It is like ordering food at a restaurant. You get an order number (the Promise). Eventually, the food arrives (the Promise resolves with a value). Or the kitchen runs out of ingredients (the Promise rejects with an error).

The three states

⏳ Pending

The operation is still in progress. No result yet. Like waiting for the API to respond.

✅ Fulfilled

The operation succeeded. The Promise now has a value. Like receiving the API data.

❌ Rejected

The operation failed. The Promise has an error. Like the API returning a 500 error or the network timing out.

Once a Promise settles (fulfilled or rejected), it stays that way. A fulfilled Promise does not suddenly become rejected. A rejected Promise does not retry on its own.

Real Scenario

You are building a dashboard that shows data from three different sources: user profile, recent orders, and notifications. You ask Cursor to fetch all three and display them.

Prompt I Would Type

Build a dashboard data loader that:
- Fetches user profile from /api/user
- Fetches recent orders from /api/orders
- Fetches notifications from /api/notifications
- All three requests should run in parallel (not sequentially)
- Handle errors gracefully — if one fails, still show the others
- Use async/await with proper error handling
- Show me what happens with and without await so I understand the difference

What AI Generated

First, the sequential version (slower, but easier to understand):

// Sequential — each request waits for the previous one
async function loadDashboardSlow() {
  try {
    const userResponse = await fetch('/api/user');
    const user = await userResponse.json();       // Wait for user...

    const ordersResponse = await fetch('/api/orders');
    const orders = await ordersResponse.json();   // Then wait for orders...

    const notifsResponse = await fetch('/api/notifications');
    const notifs = await notifsResponse.json();   // Then wait for notifications...

    return { user, orders, notifs };
  } catch (error) {
    console.error('Dashboard load failed:', error);
    throw error;
  }
}
// Total time: ~900ms (300ms + 300ms + 300ms)

Now the parallel version (faster):

// Parallel — all three requests start at the same time
async function loadDashboardFast() {
  try {
    const [userResponse, ordersResponse, notifsResponse] = await Promise.all([
      fetch('/api/user'),
      fetch('/api/orders'),
      fetch('/api/notifications')
    ]);

    const [user, orders, notifs] = await Promise.all([
      userResponse.json(),
      ordersResponse.json(),
      notifsResponse.json()
    ]);

    return { user, orders, notifs };
  } catch (error) {
    console.error('Dashboard load failed:', error);
    throw error;
  }
}
// Total time: ~300ms (all three run simultaneously)

And the resilient version (if one fails, the others still load):

// Resilient — partial failures do not break the whole dashboard
async function loadDashboardResilient() {
  const results = await Promise.allSettled([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/orders').then(r => r.json()),
    fetch('/api/notifications').then(r => r.json())
  ]);

  return {
    user: results[0].status === 'fulfilled' ? results[0].value : null,
    orders: results[1].status === 'fulfilled' ? results[1].value : null,
    notifs: results[2].status === 'fulfilled' ? results[2].value : null
  };
}

Understanding Each Part

async keyword

Putting async before a function does two things:

  1. Lets you use await inside the function.
  2. Makes the function always return a Promise, even if you return a plain value.
async function getName() {
  return 'Chuck';
}
// getName() returns Promise { 'Chuck' }, not just 'Chuck'
// You need await getName() to get the actual string

await keyword

await pauses the function until a Promise resolves and gives you the value inside:

const response = await fetch('/api/data'); // Pauses here until fetch completes
const data = await response.json();        // Pauses here until JSON parsing completes
console.log(data);                          // Now you have the actual data

Without await, you get the Promise object, not the value:

const response = fetch('/api/data'); // No await — response is Promise { <pending> }
console.log(response);              // Promise { <pending> } — not what you wanted

This is the single most common Promise bug in AI-generated code: forgetting await. The code does not crash — it just silently gives you a Promise object where you expected data.

try/catch for error handling

When an awaited Promise rejects, it throws an error. If you do not catch it, you get an "unhandled promise rejection":

// ❌ No error handling — crashes on network failure
async function fetchData() {
  const response = await fetch('/api/data'); // Throws if network fails
  return await response.json();
}

// ✅ With error handling
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error.message);
    return null; // Return a fallback instead of crashing
  }
}

.then() and .catch() (the older syntax)

Before async/await, Promises were handled with .then() and .catch():

// .then() chain — equivalent to async/await above
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Failed:', error));

Both styles work. async/await is easier to read, especially when you have multiple steps. .then() is useful for simple one-off operations. AI tools generate both, but increasingly prefer async/await.

Promise.all() vs. Promise.allSettled()

Promise.all()

Runs all Promises in parallel. Resolves when ALL succeed. Rejects immediately if ANY one fails. Use when you need all results or nothing.

Promise.allSettled()

Runs all Promises in parallel. Waits for ALL to finish, regardless of success or failure. Returns status and value/reason for each. Use when partial results are acceptable.

What AI Gets Wrong About Promises

Missing await

The #1 bug. AI generates const data = fetchData() instead of const data = await fetchData(). The code runs without errors but data is a Promise object, not the actual value. Everything downstream that tries to use data breaks in confusing ways.

No error handling

AI generates await fetch() without any try/catch. This works until the network fails, the API returns an error, or the server is slow. Then you get an unhandled promise rejection and the app crashes.

Sequential instead of parallel

AI writes three await fetch() calls in a row when they could run simultaneously with Promise.all(). This triples the loading time for no reason. If the requests do not depend on each other, they should run in parallel.

Mixing async/await with .then()

AI sometimes generates confusing hybrid code:

// ❌ Mixing styles — confusing and error-prone
async function getData() {
  const result = await fetch('/api/data')
    .then(res => res.json())
    .then(data => data.results);
  return result;
}

// ✅ Pick one style — async/await throughout
async function getData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data.results;
}

Not checking response.ok

fetch() does not reject on HTTP errors (404, 500). It only rejects on network failures. AI often writes await response.json() without checking response.ok first, which means 404 and 500 errors pass silently and try to parse an error page as JSON.

The Golden Rule of fetch()

Always check response.ok before parsing the body. fetch() resolves for any HTTP response, even 404 and 500. Only network failures cause rejection.

How to Debug Promises With AI

Console.log the Promise itself

If a variable unexpectedly shows Promise { <pending> } or [object Promise], you forgot to await it. Add await and the value will appear.

Read the rejection reason

Unhandled promise rejection errors include the reason. Read it carefully — it usually tells you exactly what failed (network error, parse error, status code, etc.).

Add temporary logging

async function fetchData() {
  console.log('Starting fetch...');
  const response = await fetch('/api/data');
  console.log('Response status:', response.status);
  console.log('Response ok:', response.ok);
  const data = await response.json();
  console.log('Parsed data:', data);
  return data;
}

This takes 30 seconds to add and immediately shows you where the chain breaks.

The debugging prompt

Debug Prompt

I'm getting [Promise { <pending> } / unhandled rejection / undefined]:
Here's my async function: [paste code]
Here's how I'm calling it: [paste the call site]
Here's the error: [paste exact error]
What's the Promise issue and how do I fix it?

Common error messages decoded

UnhandledPromiseRejection

A Promise failed and no try/catch or .catch() was there to handle it. Add error handling.

Promise { <pending> }

You are looking at a Promise object instead of its resolved value. You forgot await.

Cannot read property of undefined

Often means the variable holds a Promise or null instead of the expected data object. Check for missing await or failed fetch.

SyntaxError: Unexpected token in JSON

The server returned HTML (often a 404 page) instead of JSON. Check response.ok before calling response.json().

What to Learn Next

Next Step

Open any AI-generated JavaScript file in your project and search for await. For every await you find, check: is it inside a try/catch? If not, that is a potential crash point. Adding error handling to unprotected await calls is one of the fastest ways to make AI-generated code production-ready.

FAQ

A Promise is an object that represents the eventual result of an asynchronous operation. It starts pending and settles as either fulfilled (success with a value) or rejected (failure with an error). Promises let code wait for time-consuming operations without blocking everything else.

They handle Promises in different ways but achieve the same result. .then() chains callbacks: fetch().then(res => res.json()).then(data => ...). async/await lets you write it as sequential-looking code: const res = await fetch(); const data = await res.json(). Most AI tools prefer async/await because it is easier to read and debug.

It means a Promise was rejected (an error occurred) but no .catch() or try/catch block was in place to handle it. This is the most common async error in AI-generated code. Wrap your await calls in try/catch blocks to prevent it.

Usually because you forgot await when calling it, or the function does not have a return statement. Every async function returns a Promise. If you do not await it, you get the Promise object instead of the resolved value. If the function has no return, the Promise resolves with undefined.

Use Promise.all() when you have multiple independent async operations that can run simultaneously — for example, fetching from three APIs at the same time. It resolves when all Promises succeed, or rejects immediately if any one fails. For partial failure tolerance, use Promise.allSettled() instead.