JavaScript Promises — What is the Final Output?
This is a daily Javascript challenge from the CodeShot archive. Practice your knowledge of Promise Chain Error Catch and improve your technical interview readiness.
Promise.resolve(1)
.then(v => v + 1)
.then(v => { throw new Error("oops") })
.then(v => v + 1)
.catch(err => console.log(err.message))
Detailed Explanation
Why This Question Matters
If you've spent any time with JavaScript, you know that Promises are the backbone of almost everything we do—from fetching API data to handling file uploads. But there's a massive difference between *using* a Promise and actually understanding the Promise chain.
Most developers get tripped up by how errors propagate through a chain. They assume that if a .then() fails, the whole thing just stops, or that a .catch() only handles the very first error. In reality, the Promise chain is like a relay race: if one runner trips, the "baton" (the state of the promise) changes to "rejected," and it keeps skipping over every .then() until it finds someone who knows how to handle the crash.
Understanding this is the difference between a bug that takes five minutes to fix and a production outage that takes five hours to debug.
Understanding the Code
Let's look at the snippet:
Here is exactly what's happening under the hood, step-by-step:
1. Promise.resolve(1): We start with a resolved promise containing the value 1.
2. First .then(v => v + 1): This takes 1, adds 1, and returns 2. Since this function returns a value, the next promise in the chain is resolved with 2.
3. Second .then(v => { throw new Error("oops") }): This is where things get interesting. We aren't returning a value here; we are throwing an error. In the world of Promises, throwing an error inside a .then() handler automatically rejects the promise returned by that .then().
4. Third .then(v => v + 1): Now the chain is in a "rejected" state. A .then() handler only triggers for *resolved* promises. Because the previous step failed, JavaScript completely skips this block. It doesn't even look at it.
5. The .catch(err => console.log(err.message)): Finally, the engine finds a rejection handler. The error "oops" is passed into this block, and console.log prints the message.
Finding the Correct Answer
The correct answer is Option B: "oops".
Why not the others? A common mistake is thinking the third .then() might still run or that the error would somehow be "absorbed" without hitting the catch.
If the code had returned a value instead of throwing, the result would have been 3. But because we threw an error, the chain immediately shifted from the "success" track to the "failure" track. Once a promise is rejected, it will skip every single .then() it encounters until it hits a .catch() (or the second argument of a .then(), which acts as a catch).
Common Mistakes Developers Make
The biggest trap here is forgetting that .catch() returns a promise.
Many developers think that once a .catch() is hit, the chain is over. It's not. If you add another .then() *after* the .catch(), that .then() will execute. Why? Because the .catch() block itself returns a resolved promise (unless you explicitly throw another error or return a rejected promise inside the catch).
Another mistake is confusing throw new Error() with returning Promise.reject(). In most cases, they behave similarly inside a .then(), but if you're inside a constructor or a non-promise function, throw will just crash your app (or trigger a global uncaught exception) instead of being caught by your promise chain.
Real-World Usage
In a real production app, you'll see this pattern everywhere—especially in API middleware.
Imagine you have a sequence of operations:
1. Validate a user token.
2. Fetch user profile.
3. Fetch user preferences.
4. Update the UI.
You don't want to write a separate error handler for every single single step. Instead, you chain them together. If the token validation fails, there's no point in trying to fetch the profile or preferences. By throwing an error at the first step, you effectively "short-circuit" the rest of the logic and jump straight to a single, global error handler that shows a "Session Expired" toast notification to the user.
This makes your code cleaner and prevents "callback hell" or deeply nested try-catch blocks.
Key Takeaways
- Promises are a chain: Each .then() returns a new promise.
- Rejection skips .then(): Once a promise is rejected, all subsequent .then() blocks are ignored until a .catch() is found.
- Errors are "bubbled": Throwing an error inside a handler automatically rejects the promise.
- Catch is a recovery point: A .catch() handles the error and, by default, puts the promise chain back into a "resolved" state for any following handlers.
Why this matters
Understanding Promise Chain Error Catch is crucial for passing technical interviews. In real-world applications, this concept often leads to subtle bugs if not handled correctly. For more details, you can always refer to the official MDN Documentation.