DAILY NEWS

Stay Ahead, Stay Informed – Every Day

Advertisement

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.

πŸ“ˆ 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

Never swallow errors silently. A hidden bug is a time bomb.

Always tell the user something. Frozen UI is worse than an error message.

Log with context, not just a message β€” what failed, who triggered it, when.

Not all errors are equal β€” 404 β‰  500 β‰  NetworkError. Handle them differently.

Retryable β‰  always retry β€” client errors (4xx) should fail fast.

Leave the system in a valid state. Roll back or compensate when operations are partial.

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. πŸ› οΈ



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *