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
undefinedeven 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:
- Lets you use
awaitinside the function. - 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
- What Is Async/Await? — A deeper dive into the syntax that makes Promises readable.
- What Is JavaScript? — Understand the language that Promises are built into.
- What Is an API? — API calls are the most common reason you use Promises.
- What Is Error Handling? — The broader context for try/catch beyond Promises.
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.