One question that comes up a lot when learning JavaScript async code is this: when you return a value inside .then(), does it get wrapped in Promise.resolve(value) automatically? And if you throw something, does it become Promise.reject(error)?
Short answer: yes, exactly right.
It's one of those things that sounds small but really clicks everything into place once you get it. Let me break it all down.
What even is a Promise?
Before ES6 (back when callbacks ruled everything), async code looked like this:
getUserFromDB(userId, function(err, user) {
if (err) {
handleError(err);
return;
}
getPostsByUser(user.id, function(err, posts) {
if (err) {
handleError(err);
return;
}
// imagine this going 5 levels deep...
});
});
This is called callback hell — and it's as bad as it sounds.
A Promise is a cleaner way to handle async operations. Think of it like ordering food at a restaurant. You place your order (start the async operation), and instead of standing at the kitchen window waiting, the waiter gives you a number (the Promise). You go sit down and do other things. When the food is ready, they call your number.
A Promise is in one of three states:
- Pending — still waiting (the kitchen is still cooking)
- Fulfilled — success, you have a value (food arrived)
- Rejected — something went wrong (they're out of your order)
The .then() chaining trick
Here's the interesting part. When you chain .then() calls, each .then() returns a new Promise. And what that Promise resolves or rejects with depends on what happens inside the callback.
The rules are simple:
What you do inside .then()
|
What the next Promise gets |
|---|---|
return someValue |
Wraps it: Promise.resolve(someValue)
|
return anotherPromise |
Waits for that promise and uses its result |
throw new Error(...) |
Wraps it: Promise.reject(error)
|
fetchUser(1)
.then(user => {
return user.name; // becomes Promise.resolve("John")
})
.then(name => {
console.log(name); // "John"
throw new Error("Oops!"); // becomes Promise.reject(Error("Oops!"))
})
.then(() => {
console.log("This won't run");
})
.catch(err => {
console.log(err.message); // "Oops!"
});
This chaining is powerful because you avoid nesting — each .then() hands off to the next one cleanly.
async/await is just Promises in disguise
async/await isn't a different system — it's syntax sugar on top of Promises. The same rules apply.
async function getUser() {
return "John"; // same as: return Promise.resolve("John")
}
async function fail() {
throw new Error("Something broke"); // same as: return Promise.reject(...)
}
And await just pauses execution until the Promise settles:
async function main() {
try {
const user = await fetchUser(1); // waits for Promise to resolve
console.log(user.name);
} catch (err) {
console.log("Error:", err.message); // catches rejections
}
}
The try/catch with await is equivalent to .then().catch() — same idea, cleaner look.
Real use case: Fetching data from an API
Here's a practical example — fetching a GitHub user's profile and then their repos:
// With .then() chaining
function getGithubInfo(username) {
fetch(`https://api.github.com/users/${username}`)
.then(response => response.json()) // parse JSON (returns a Promise)
.then(user => {
console.log("User:", user.name);
return fetch(`https://api.github.com/users/${username}/repos`);
})
.then(response => response.json())
.then(repos => {
console.log("Repos:", repos.length);
})
.catch(err => {
console.error("Something went wrong:", err.message);
});
}
// Same thing with async/await — cleaner!
async function getGithubInfo(username) {
try {
const userResponse = await fetch(`https://api.github.com/users/${username}`);
const user = await userResponse.json();
console.log("User:", user.name);
const reposResponse = await fetch(`https://api.github.com/users/${username}/repos`);
const repos = await reposResponse.json();
console.log("Repos:", repos.length);
} catch (err) {
console.error("Something went wrong:", err.message);
}
}
Both do the same thing. Most people prefer async/await because it reads like regular synchronous code.
Quick summary
- A Promise represents a future value from an async operation
-
.then()always returns a new Promise — whatever youreturnbecomesPromise.resolve(...), whatever youthrowbecomesPromise.reject(...) -
async/awaitis built on top of Promises — they are the same thing underneath - Use
.catch()ortry/catchto handle errors
If Promises still feel fuzzy, that's okay. The best way to solidify this is to build something — try fetching data from a public API, chain a few .then() calls, and break things on purpose to see what happens.
You'll get it. It just takes repetition.
Keep asking questions — they help everyone learn.
Top comments (0)