O padrão mais perigoso que vejo em código de produção não é um bug: é um catch vazio. Um erro silenciado não desaparece, apenas reaparece mais tarde e mais longe de onde aconteceu.

Falhe rápido e alto

Se algo não deveria acontecer, não esconda. Um erro visível em desenvolvimento é um bug que você corrige hoje; um erro engolido é um ticket de suporte na semana que vem.

// ❌ o erro evapora
try {
  await saveUser(user);
} catch {}

// ✅ pelo menos você sabe o que aconteceu
try {
  await saveUser(user);
} catch (err) {
  logger.error('saveUser failed', { userId: user.id, err });
  throw err;
}

Modele o erro, não improvise

No frontend, lançar strings ou any te deixa cego. Prefiro tipos de erro explícitos que digam o que falhou e como se recuperar:

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

Quem consome a função é obrigado pelo tipo a tratar cada caso. Essa ideia me levou a publicar o http-sentinel, justamente para estruturar erros HTTP no cliente.

A regra

Um erro ou é tratado (com uma ação concreta) ou é propagado (para cima, com contexto). O que você nunca faz é ignorá-lo. O silêncio no tratamento de erros sempre é pago com juros.