The most dangerous pattern I see in production code isn’t a bug: it’s an empty catch. A silenced error doesn’t go away, it just reappears later and farther from where it happened.

Fail fast and loud

If something shouldn’t happen, don’t hide it. An error visible in development is a bug you fix today; a swallowed error is a support ticket next week.

// ❌ the error evaporates
try {
  await saveUser(user);
} catch {}

// ✅ at least you know what happened
try {
  await saveUser(user);
} catch (err) {
  logger.error('saveUser failed', { userId: user.id, err });
  throw err;
}

Model the error, don’t improvise it

On the frontend, throwing strings or any leaves you blind. I prefer explicit error types that say what failed and how to recover:

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: 'network' | 'unauthorized' | 'not_found' };

Whoever consumes the function is forced by the type to handle each case. This idea led me to publish http-sentinel, precisely to structure HTTP errors on the client.

The rule

An error is either handled (with a concrete action) or propagated (upward, with context). What you never do is ignore it. Silence in error handling is always paid back with interest.