Leaderboard
Javascript May 28, 2026
Javascript Async Await Vs Promise Chain Error Handling

JavaScript Async Error Handling — .catch() vs try/catch?

This is a daily Javascript challenge from the CodeShot archive. Practice your knowledge of Async Await Vs Promise Chain Error Handling and improve your technical interview readiness.

// Option A
fetch(url).then(handleData).catch(handleError)

// Option B
try {
  const res = await fetch(url)
  handleData(res)
} catch(err) {
  handleError(err)
}
A Option A — chaining is cleaner
B Option B — easier to read and handles errors in the same familiar way as sync code
C Identical in every way
D Option A catches more errors

Detailed Explanation

Why This Question Matters

If you've spent any time in JavaScript, you've seen both .then() chains and async/await. On the surface, they do the same thing: they handle a Promise. But when things go wrong—which they always do—the way you handle those errors can be the difference between a graceful fallback and a crashed process or a "silent failure" that takes you three hours to debug.

The confusion usually stems from the fact that async/await is essentially syntactic sugar for Promises. Since they're doing the same work under the hood, a lot of devs assume they're interchangeable. In reality, the way they interact with the call stack and scope makes one significantly more robust for error handling than the other.

Understanding the Code

Let's look at the two patterns.

Option A: The Promise Chain

fetch(url).then(handleData).catch(handleError)

Here, we're using a fluent interface. We tell JavaScript: "Start this request, and when it finishes, run handleData. If anything in that chain fails, jump to handleError." It's concise, but it's a "fire and forget" style of execution.

Option B: The Try/Catch Block

try {
const res = await fetch(url)
handleData(res)
} catch(err) {
handleError(err)
}

This approach treats asynchronous code as if it were synchronous. We await the result of the fetch, and if that promise rejects (or if handleData throws an error), the execution immediately jumps into the catch block.

Finding the Correct Answer

The winner here is Option B. But why?

If you're just doing a single fetch, Option A looks fine. But real-world apps aren't that simple. Imagine you need to fetch a user, then fetch their posts based on that user ID, then fetch the comments for those posts.

With .then(), you end up in "Promise Hell"—deeply nested chains or a long list of .then() calls where it becomes unclear which specific step triggered the .catch(). If an error happens inside handleData, the .catch() will grab it, but you lose the clean, linear flow of execution.

Option B is superior because it provides granular control. By using try/catch, you can wrap specific parts of your logic. If you have three different API calls, you can wrap each in its own try/catch to handle a "User Not Found" error differently than a "Database Timeout" error.

More importantly, try/catch handles both asynchronous errors (like a network failure) and synchronous errors (like trying to access a property of undefined inside handleData). In Option A, if you have a synchronous error outside the promise chain, your .catch() won't save you.

Common Mistakes Developers Make

The biggest mistake I see is the "Fetch Trap."

Many developers think that if the server returns a 404 Not Found or a 500 Internal Server Error, the catch block will trigger. It won't.

fetch() only rejects the promise if the network request itself fails (like a DNS error or a lost connection). If the server responds with a 404, that's technically a successful HTTP response. To handle this properly in an async/await flow, you have to manually check the ok property:

const res = await fetch(url);
if (!res.ok) {
  throw new Error(HTTP error! status: ${res.status});
}
const data = await res.json();

Another common slip-up is forgetting that await must be inside an async function. Beginners often try to use it at the top level of a script, which will throw a syntax error unless they're using ES Modules with top-level await.

Real-World Usage

In a production environment, we rarely just call handleError(err). We usually have a global error-handling strategy.

In a large React or Vue app, you'll often see a pattern where a service layer handles the API call and a controller layer handles the try/catch. This keeps the UI logic clean.

For example, in a Node.js/Express backend, using async/await with try/catch is non-negotiable. If a promise rejects and you don't have a .catch() or a try/catch block, you get an unhandledRejection. In older versions of Node, this was a warning; in newer versions, it can crash your entire process.

Key Takeaways

  • Prefer async/await for readability and better stack traces.
  • Use try/catch to handle both async network failures and sync logic errors in one place.
  • Remember the Fetch quirk: fetch() doesn't throw on 4xx or 5xx errors; you have to check res.ok.
  • Avoid long .then() chains. They're harder to debug and lead to "pyramid of doom" code.
  • Be explicit. Wrapping your logic in try/catch makes it obvious to anyone reading your code exactly where the "danger zone" is.
  • Why this matters

    Understanding Async Await Vs Promise Chain Error Handling 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.

    📝
    Reviewed by CodeShot Editorial
    Every challenge is code-reviewed by senior developers to ensure accuracy and real-world relevance. Learn more.

    Ready for your shot?

    Join thousands of developers solving one logic puzzle every morning.

    Solve Today's Challenge →