1. First, what does asynchronous mean?
JavaScript usually runs your code on one main thread. It executes one piece of JavaScript at a time, in order. Some operations, however, take an unpredictable amount of time: a network request, a timer, reading a file, or asking a database for data.
If JavaScript waited for each of those operations to finish before doing anything else, a slow request could freeze an entire page. Instead, the runtime starts the operation and lets other work continue. Later, when the result is ready, JavaScript runs the function that handles it.
console.log("1. Start");
setTimeout(() => {
console.log("3. Timer finished");
}, 0);
console.log("2. Keep going");
// Output:
// 1. Start
// 2. Keep going
// 3. Timer finished
The timer callback runs later, even with a delay of zero. Network and file operations follow the same broad idea: they complete outside the immediate sequence of synchronous statements, then notify your code.
2. Why do Promises exist?
Before Promises became a standard way to handle asynchronous results, JavaScript commonly used callbacks: functions passed to another function to be called when work completed.
getUser(userId, function (error, user) {
if (error) {
showError(error);
return;
}
getOrders(user.id, function (error, orders) {
if (error) {
showError(error);
return;
}
showOrders(orders);
});
});
A callback is useful for one operation, but chaining many dependent operations nests code and repeats error handling. This shape is sometimes called callback hell. It is not that callbacks are inherently bad; rather, callbacks alone do not provide a consistent, composable representation of a future result.
A Promise gives that future result a standard object. The object can be returned from a function, passed around, chained with other work, and handled with a common success and failure path. It makes dependencies between asynchronous steps explicit without deeply nested callbacks.
3. The three Promise states
A Promise is always in one of three states:
- Pending: the operation has not finished yet.
- Fulfilled: the operation succeeded and produced a value.
- Rejected: the operation failed and produced a reason, usually an error.
A Promise starts pending and settles exactly once: either fulfilled or rejected. Once settled, its state and result cannot be changed. A fulfilled or rejected Promise is often described as settled.
4. Creating and using a Promise
Most of the time, you will consume Promises returned by
APIs such as fetch(). You can also create
one with the Promise constructor when you
need to adapt a callback-based API or represent a
custom asynchronous operation.
const wait = (milliseconds) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve("The wait is over");
}, milliseconds);
});
};
wait(500).then((message) => {
console.log(message);
});
The constructor receives an executor function
with two functions: resolve and
reject. Call resolve(value)
when the work succeeds, or reject(error)
when it fails. The executor itself runs immediately;
here the timer is what schedules the later completion.
Handle success with .then()
Calling .then(onFulfilled) registers a
handler for a fulfilled Promise. The handler receives
the fulfilled value. It does not run inline at the
exact point where the Promise settles; Promise handlers
are scheduled as microtasks, after the current
synchronous JavaScript has finished.
const ready = Promise.resolve("Ready");
ready.then((value) => console.log(value));
console.log("Synchronous code runs first");
// Output:
// Synchronous code runs first
// Ready
Handle failure with .catch()
Use .catch(onRejected) to handle a
rejection. It is shorthand for
.then(undefined, onRejected).
const loadSettings = () => {
return Promise.reject(new Error("Settings are unavailable"));
};
loadSettings()
.then((settings) => renderSettings(settings))
.catch((error) => {
console.error("Could not load settings:", error.message);
showSettingsError();
});
Chain work by returning values
Every call to .then() returns a new Promise.
If its handler returns a regular value, the next
handler receives that value. If it returns another
Promise, the chain waits for that Promise to settle.
This is what makes a sequence of asynchronous steps
compose cleanly.
getUser(userId)
.then((user) => getOrders(user.id))
.then((orders) => renderOrders(orders))
.catch((error) => showError(error));
Throwing inside a handler rejects the Promise returned
by that handler, so the error flows to the nearest
later .catch(). Returning a value from
.catch() recovers the chain; throwing
again keeps it rejected.
5. Async and await: a clearer way to write chains
An async function always returns a Promise.
If it returns a normal value, that value becomes the
Promise's fulfillment value. If it throws, the returned
Promise rejects. Inside an async function,
await pauses that function until a
Promise settles, then gives you its value or throws its
rejection reason.
async function showUserOrders(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
renderOrders(orders);
} catch (error) {
showError(error);
}
}
This reads much like synchronous code, but
await does not block the browser's main
thread. It suspends only the current async function;
other JavaScript can continue to run.
Use try/catch for errors from awaited
operations. If you omit the catch, the async function's
returned Promise rejects, so its caller must handle
that rejection.
6. Real-world Promise patterns
Fetch data from an API
fetch() returns a Promise that fulfills
with a Response. One important detail:
HTTP error statuses such as 404 or 500 do not usually
reject the fetch Promise. Check
response.ok and throw when the status is
not successful.
async function getProducts() {
const response = await fetch("/api/products");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}
getProducts()
.then((products) => renderProducts(products))
.catch((error) => showError(error));
Run independent requests in parallel
If two operations do not depend on each other, start
both before waiting. Promise.all()
fulfills when all inputs fulfill, preserving their
input order in the result array. It rejects as soon as
one input rejects.
async function loadDashboard() {
const [profile, notifications] = await Promise.all([
fetchJson("/api/profile"),
fetchJson("/api/notifications"),
]);
renderDashboard(profile, notifications);
}
By contrast, awaiting each request before starting the next makes independent work sequential:
// Sequential: the second request starts after the first finishes.
const profile = await fetchJson("/api/profile");
const notifications = await fetchJson("/api/notifications");
// Parallel: both requests start before either is awaited.
const [profile, notifications] = await Promise.all([
fetchJson("/api/profile"),
fetchJson("/api/notifications"),
]);
Collect every outcome with allSettled
Sometimes each result is useful even if another
operation fails. Promise.allSettled()
waits for every input and returns a status and value or
reason for each one.
const results = await Promise.allSettled([
fetchJson("/api/recommendations"),
fetchJson("/api/recently-viewed"),
]);
for (const result of results) {
if (result.status === "fulfilled") {
displayWidget(result.value);
} else {
console.warn("A dashboard widget could not load:", result.reason);
}
}
This is useful for optional dashboard panels, batch tasks, and independent uploads where you want to report each success or failure instead of stopping at the first rejection.
Race for the first settled result
Promise.race() settles with the first input
Promise that settles, whether it fulfills or rejects.
It can implement a timeout, but a race alone does
not cancel the slower operation.
function withTimeout(promise, milliseconds) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error("Operation timed out"));
}, milliseconds);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timer);
});
}
const result = await withTimeout(fetch("/api/report"), 5000);
If the underlying work should actually stop, use an
API that supports cancellation (for example,
AbortController with
fetch) and abort it when the timeout
expires.
Convert a callback API
A Promise can wrap an older callback-based interface,
allowing the rest of the application to use
async/await.
function readConfigAsPromise(readConfig) {
return new Promise((resolve, reject) => {
readConfig((error, config) => {
if (error) {
reject(error);
return;
}
resolve(config);
});
});
}
For Node.js callback APIs that follow the standard
error-first convention, Node also provides
util.promisify().
7. Common mistakes to avoid
Forgetting to return a Promise in a chain
// Incorrect: the next then does not wait for saveUser().
getUser(id).then((user) => {
saveUser(user);
}).then(() => {
showSavedMessage();
});
// Correct: return the Promise so the chain waits.
getUser(id).then((user) => {
return saveUser(user);
}).then(() => {
showSavedMessage();
});
Starting work that is never awaited or returned
An unhandled rejected Promise can produce a runtime warning and leave the user without feedback. If you start asynchronous work, make sure its rejection is handled, returned to a caller that handles it, or intentionally reported.
Using Promise.all() for independent best-effort work
Promise.all() is right when every result is
required. If failures are independent and you need the
outcome of each task, choose
Promise.allSettled() instead.
Thinking Promise means “background thread”
Promises coordinate asynchronous results; they do not move CPU-heavy JavaScript to another thread. A long-running loop can still block rendering. For expensive computation, consider a Web Worker.
8. The mental model to keep
- A Promise represents a result that may be available now, later, or not at all.
- It settles once: fulfilled with a value or rejected with a reason.
-
.then()transforms and chains results;.catch()handles failures. -
async/awaitis syntax for working with Promises, not a different asynchronous system. -
Use
Promise.all()for required parallel work andallSettled()when you need every independent outcome.
Once you recognize a Promise as a value that describes
future work, APIs and patterns that once felt
mysterious become a lot more predictable. Start by
following the value through each
.then() or await, and make
sure every failure has a deliberate place to go.