DEV Community

Cover image for Your trycatch sucks - lets fix it
Shuvo
Shuvo

Posted on

Your trycatch sucks - lets fix it

You're not handling errors. You're hiding them.

Every app crashes. Every API fails. Every database hiccups at 2am on a Friday.
The difference between a good dev and a great one? What happens next.

Let's roast your error handling β€” then make it legendary.


🀦 Level 0: The "Trust Me Bro" Dev

No try/catch at all. Just vibes.

const data = await fetchUserData(userId);
console.log(data.profile.name); // πŸ’₯ TypeError: Cannot read properties of undefined
Enter fullscreen mode Exit fullscreen mode

The crime: One bad response nukes the entire app. Users see a white screen. You get a 3am Slack ping.


🐣 Level 1: The Junior β€” "I Googled try/catch"

try {
  const data = await fetchUserData(userId);
  setUser(data);
} catch (err) {
  console.log(err); // πŸ‘ˆ and... that's it. shipped.
}
Enter fullscreen mode Exit fullscreen mode

What's wrong here?

  • console.log in production helps nobody β€” users still see a broken UI
  • No distinction between a 404 and a 500 β€” every error is treated the same
  • The error disappears into the void (or a DevTools tab nobody has open)
  • err might be null, a string, or an Error object β€” you're not checking

The mindset: "At least it won't crash." β€” Yeah, it just silently breaks instead. Cool.

GitMission Preview


πŸ“ˆ Level 2: The Mid-Level β€” "I've Been Burned Before"

Now we're thinking. You've seen production fires. You have trust issues with APIs. Good.

2a β€” Typed errors, real messages

try {
  const data = await fetchUserData(userId);
  setUser(data);
} catch (err) {
  if (err instanceof NetworkError) {
    showToast("Connection lost. Check your internet.", "warning");
  } else if (err.status === 404) {
    showToast("User not found.", "error");
  } else {
    showToast("Something went wrong. We're on it.", "error");
    logger.error("[fetchUserData]", { userId, err }); // πŸ‘ˆ goes to Sentry/Datadog
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Users get useful feedback, not a frozen screen
βœ… Engineers get structured logs, not a haystack of console.logs


2b β€” Undo previous operations (the "atomic mindset")

Imagine you're updating a user's profile and their avatar. Step 1 succeeds. Step 2 fails.
Congrats β€” your user now has a corrupted half-state.

let previousProfile = null;

try {
  previousProfile = await getProfile(userId); // snapshot
  await updateProfile(userId, newProfileData); // step 1
  await uploadAvatar(userId, newAvatar);        // step 2 πŸ’₯ fails here
} catch (err) {
  logger.error("Profile update failed", { err });

  // ↩️ Roll back step 1 since step 2 failed
  if (previousProfile) {
    await updateProfile(userId, previousProfile);
  }

  showToast("Update failed. Your profile has been restored.", "warning");
}
Enter fullscreen mode Exit fullscreen mode

βœ… Users never see broken half-state
βœ… Rollback is explicit, not accidental


2c β€” Wrap it in a clean utility (stop repeating yourself)

Tired of writing try/catch 50 times? Make a helper:

// utils/tryCatch.js
export async function tryCatch(fn, fallback = null) {
  try {
    const result = await fn();
    return [result, null];
  } catch (err) {
    return [fallback, err];
  }
}

// Usage β€” clean, flat, readable
const [user, err] = await tryCatch(() => fetchUserData(userId));

if (err) {
  showToast("Couldn't load user.", "error");
  return;
}

setUser(user);
Enter fullscreen mode Exit fullscreen mode

βœ… No more deeply nested try/catch pyramids
βœ… Forces you to handle the error at call site β€” can't ignore it


🧠 Level 3: The Senior β€” "I've Seen Things"

You don't just catch errors. You anticipate them. You build systems that heal themselves.

3a β€” Retry queue with exponential backoff

Networks are flaky. Don't give up on the first failure.

async function fetchWithRetry(fn, { retries = 3, delay = 500 } = {}) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const isLast = attempt === retries;
      const isRetryable = err.status >= 500 || err instanceof NetworkError;

      if (isLast || !isRetryable) throw err; // don't retry 401s or 404s

      const backoff = delay * 2 ** (attempt - 1); // 500ms β†’ 1s β†’ 2s
      logger.warn(`Attempt ${attempt} failed. Retrying in ${backoff}ms...`);
      await sleep(backoff);
    }
  }
}

// Usage
const data = await fetchWithRetry(() => fetchUserData(userId));
Enter fullscreen mode Exit fullscreen mode

βœ… Temporary blips are invisible to users
βœ… Smart: retries server errors, not client errors (no point retrying a 401)


3b β€” Circuit breaker (stop hammering a dead service)

A retry queue is great β€” unless the whole service is down. Then you're just DDoS-ing a corpse.

class CircuitBreaker {
  constructor(threshold = 5, timeout = 30_000) {
    this.failures = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = "CLOSED"; // CLOSED = healthy, OPEN = tripped, HALF_OPEN = testing
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === "OPEN") {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit open β€” service unavailable");
      }
      this.state = "HALF_OPEN";
    }

    try {
      const result = await fn();
      this.reset();
      return result;
    } catch (err) {
      this.recordFailure();
      throw err;
    }
  }

  recordFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = "OPEN";
      this.nextAttempt = Date.now() + this.timeout;
      logger.error("πŸ”΄ Circuit breaker TRIPPED");
    }
  }

  reset() {
    this.failures = 0;
    this.state = "CLOSED";
  }
}

// Usage
const userServiceBreaker = new CircuitBreaker();
const data = await userServiceBreaker.call(() => fetchUserData(userId));
Enter fullscreen mode Exit fullscreen mode

βœ… Failing fast is kind β€” users get an error immediately, not after 10s of retrying
βœ… Gives the downstream service a breather to recover


3c β€” Structured error classes (errors that mean something)

Stop throwing raw strings or generic Errors. Give your errors context.

class AppError extends Error {
  constructor(message, { code, statusCode = 500, context = {}, retryable = false } = {}) {
    super(message);
    this.name = "AppError";
    this.code = code;
    this.statusCode = statusCode;
    this.context = context;
    this.retryable = retryable;
    this.timestamp = new Date().toISOString();
  }
}

// Subclass for specificity
class AuthError extends AppError {
  constructor(message, context) {
    super(message, { code: "AUTH_ERROR", statusCode: 401, context, retryable: false });
  }
}

class ServiceUnavailableError extends AppError {
  constructor(service, context) {
    super(`${service} is unavailable`, { code: "SERVICE_DOWN", statusCode: 503, context, retryable: true });
  }
}

// Throwing
throw new ServiceUnavailableError("UserService", { userId, attempt: 3 });

// Catching
catch (err) {
  if (err instanceof AuthError) {
    redirectToLogin();
  } else if (err instanceof AppError && err.retryable) {
    retryQueue.add(err);
  } else {
    logger.error(err.code, err.context);
    showToast("Unexpected error. Engineers notified.");
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Catch blocks can make decisions, not just log and pray
βœ… Every error carries its own context β€” no more guessing what happened


πŸ—ΊοΈ The Full Picture

What you do Junior Mid Senior
Catch errors βœ… βœ… βœ…
Inform the user ❌ βœ… βœ…
Send to a logger ❌ βœ… βœ…
Typed/structured errors ❌ ⚠️ partial βœ…
Rollback on failure ❌ βœ… βœ…
Retry transient errors ❌ ❌ βœ…
Circuit breaker ❌ ❌ βœ…
Errors are self-describing ❌ ❌ βœ…

βœ… The Golden Rules

  1. Never swallow errors silently. A hidden bug is a time bomb.
  2. Always tell the user something. Frozen UI is worse than an error message.
  3. Log with context, not just a message β€” what failed, who triggered it, when.
  4. Not all errors are equal β€” 404 β‰  500 β‰  NetworkError. Handle them differently.
  5. Retryable β‰  always retry β€” client errors (4xx) should fail fast.
  6. Leave the system in a valid state. Roll back or compensate when operations are partial.
  7. Your catch block is business logic β€” treat it that way.

"The mark of a great engineer isn't writing code that never fails.

It's writing code that fails gracefully."

Now go fix your try/catches. πŸ› οΈ

GitMission Preview

Top comments (0)