then()
ES6+Attaches callbacks for the resolution and/or rejection of the Promise.
Syntax
promise.then(onFulfilled, onRejected)Parameters
onFulfilled Function A function called when the Promise is fulfilled
onRejected Function optionalA function called when the Promise is rejected
Return Value
A new Promise that resolves to the return value of the callback
Examples
const promise = Promise.resolve(42);
promise.then(value => {
console.log(value);
}); 📌 When to Use
Use then() whenever you need to react to the eventual result of an asynchronous operation without blocking the rest of your program. It is the fundamental building block of promise-based code: every fetch call, database query, or timer wrapped in a promise ultimately delivers its value through then(). Reach for it when you want to transform a resolved value before passing it along, because each call returns a brand-new promise that resolves with whatever your callback returns — this is what makes multi-step data pipelines possible. It is also the right tool when you must sequence dependent async steps: request a user, then use the user id to request their orders, then compute a summary from both. In codebases that cannot use async/await — older build targets, libraries that must support legacy environments, or code written before ES2017 — then() is the primary way to consume promises, so understanding it is essential for maintaining existing JavaScript. Finally, then() shines when you build chains dynamically, for example reducing an array of tasks into one sequential pipeline, something that is awkward to express with await alone. If your logic involves heavy branching or try/catch-style recovery, prefer async/await and keep then() for simple linear transformations.
⚠️ Common Mistakes
Forgetting to return a value from a then() callback. The next handler in the chain receives undefined instead of your data, and the bug typically surfaces far away from its cause as a confusing 'cannot read properties of undefined' error. Every callback that produces something the next step needs must end with a return statement (or use a concise arrow body with no braces).
Leaving the chain without a rejection handler. An error thrown anywhere in the chain travels silently to the end; if nothing catches it, browsers fire the unhandledrejection event and modern Node.js terminates the process with a non-zero exit code. Always terminate chains with catch(), even if it only logs the failure.
Nesting then() calls inside each other instead of returning the inner promise. This recreates the callback pyramid promises were designed to eliminate, and it breaks unified error handling because inner rejections never reach the outer catch(). Return the inner promise and the chain flattens automatically.
Assuming the second argument of then() catches errors thrown by the first. The onRejected callback only handles failures from earlier in the chain — if onFulfilled itself throws, that error skips its sibling handler and propagates to the next link. Use a separate catch() when you need to cover both cases.
Expecting then() callbacks to run synchronously. Handlers are always scheduled on the microtask queue, so code placed after the then() call runs first even when the promise is already resolved. Relying on immediate execution leads to reading state that has not been set yet.
Passing a non-function to then(), such as the result of calling a function — then(handle()) instead of then(handle). Non-function arguments are silently ignored and the value passes straight through, so the code appears to work while your handler actually ran at the wrong time, during chain construction.
✅ Best Practices
Prefer async/await for new code with branching or error recovery, but stay fluent in then() — most library documentation, older codebases, and interview questions still use chain style, and the two interoperate freely since async functions return promises.
Terminate every chain with catch() rather than relying on then()'s second argument, so a single handler covers failures from every step, including errors thrown inside your own fulfillment callbacks.
Keep each callback small and single-purpose. A chain like .then(parseResponse).then(normalizeUser).then(renderProfile) reads like a description of the data flow, and each named step is trivially unit-testable in isolation.
Return promises from inside callbacks instead of starting detached side chains. Anything you return is awaited before the next link runs, which keeps ordering deterministic and routes inner failures to the outer error handler.
Avoid mixing await and long then() chains inside the same function. Pick one style per function; mixing them makes control flow and error paths noticeably harder to trace during code review and debugging.
⚡ Performance Notes
Every then() call allocates a new Promise object and registers reaction records, so a chain of five handlers creates five promises. The callbacks never run synchronously: when a promise settles, its handlers are pushed onto the microtask queue, which the engine drains completely after the current call stack empties and before any rendering or setTimeout (macrotask) work. A long chain therefore adds one microtask hop per link — each callback runs in its own turn of the queue. For typical UI and network code this overhead is measured in microseconds and is irrelevant next to I/O latency. It only matters in hot loops that create thousands of short-lived promises per frame; there, collapsing several transformation steps into a single then() callback reduces allocations and queue churn. Also remember that microtasks can starve rendering: a chain that keeps scheduling more microtasks blocks paints just like synchronous code would.
🌍 Real World Example
API Data Transformation Pipeline
A common production pattern: fetch a user record, validate the HTTP status, parse the JSON body, request the user's posts, and merge everything into one view model. Each then() stage does exactly one job and returns its result to the next, so the pipeline reads top to bottom like a recipe. Notice how the inner fetch for posts is returned into the chain: that keeps the final handler waiting until both requests finish, and it lets a single catch() at the call site handle a failure in either request.
function fetchUserWithPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error('User not found');
return response.json();
})
.then(user => {
return fetch(`/api/users/${userId}/posts`)
.then(res => res.json())
.then(posts => ({ ...user, posts }));
})
.then(userWithPosts => {
// Transform data
return {
...userWithPosts,
postCount: userWithPosts.posts.length,
recentPost: userWithPosts.posts[0] || null
};
});
}
fetchUserWithPosts(1).then(console.log).catch(console.error);