Error handling

Escopo: JavaScript. Idiomas específicos deste ecossistema.

Erros bem estruturados separam o que é problema de negócio (regra violada, recurso inexistente) do que é falha técnica (timeout, banco fora). Cada categoria pede tratamento diferente: a primeira vira resposta semântica ao chamador; a segunda vira log, métrica e retry. try/catch existe para capturar, nunca para esconder.

Conceitos fundamentais

ConceitoO que é
Error (classe de erro)Classe nativa do JS; toda exceção deve estender ou usar uma subclasse
custom error (erro customizado)Subclasse de Error com nome semântico (NotFoundError, ConflictError); permite catch por tipo
business error (erro de negócio)Regra de domínio violada; chamador precisa saber para responder
technical error (erro técnico)Falha de infraestrutura (rede, banco, timeout); chamador raramente pode tratar
stack trace (rastro de chamadas)Lista de funções chamadas até o ponto do erro; preservar é essencial para debug
error cause (causa do erro)Erro original encapsulado em um novo (new Error('msg', { cause: original }))
fail fast (falhar rápido)Lançar erro no menor escopo possível; evita estado corrompido se propagando
swallow (engolir)Capturar erro sem tratar nem propagar; anti-padrão clássico

Múltiplos tipos de retorno

❌ Ruim: null, undefined, false e objeto na mesma função
function processOrder(order) {
  if (!order) return null;
  if (order.items.length === 0) return undefined;

  if (order.customer.defaulted) return false;

  return { success: true, order };
}

// quem chama não sabe o que esperar
const result = processOrder(order);
if (result) { /* ... */ }           // false passa, undefined também

if (result !== null) { /* ... */ }  // e undefined?
✅ Bom: contrato consistente, sempre o mesmo formato
function processOrder(order) {
  if (!order) throw new ValidationError({ message: "Order is required." });
  if (order.items.length === 0) throw new ValidationError({ message: "Order has no items." });

  if (order.customer.defaulted) throw new BusinessError({ message: "Customer has unpaid debts." });

  const processedOrder = { success: true, order };

  return processedOrder;
}

Erro como string

❌ Ruim: string solta, impossível tratar com instanceof
async function findUser(id) {
  const user = await db.query(id);

  if (!user) {
    throw "User not found"; // sem tipo, sem contexto
  }

  return user;
}
✅ Bom: erros tipados, identificáveis e tratáveis
async function findUser(id) {
  const user = await userRepository.findById(id);
  if (!user) throw new NotFoundError({ message: `User ${id} not found.` });

  return user;
}

BaseError: abstração centralizada

❌ Ruim: throw com string solta, sem tipo, sem contrato
// errors.js: não existe, cada módulo lança o que quiser
async function findUser(id) {
  const user = await db.query(id);
  if (!user) throw "User not found"; // sem tipo, não dá para instanceof

  return user;
}

async function processOrder(orderId) {
  try {
    const order = await getOrder(orderId);
    return order;
  } catch (error) {
    console.log(error); // engole o erro, não relança
    return null;
  }
}
✅ Bom: contrato único para todos os erros da aplicação
// errors.js
export class BaseError extends Error {
  constructor({ name, message, action, statusCode, cause }) {
    super(message, { cause });
    this.name = name || "BaseError";

    this.action = action || "Contact support.";
    this.statusCode = statusCode || 500;
  }

  toJSON() {
    const envelope = {
      error: {
        name: this.name,
        message: this.message,
        action: this.action,
        statusCode: this.statusCode,
      },
    };
    return envelope;
  }
}

export class NotFoundError extends BaseError {
  constructor({ message, action, cause } = {}) {
    super({
      name: "NotFoundError",
      message: message || "Resource not found.",
      action: action || "Check if the resource exists.",
      statusCode: 404,
      cause,
    });
  }
}

export class ValidationError extends BaseError {
  constructor({ message, action, cause } = {}) {
    super({
      name: "ValidationError",
      message: message || "Invalid input.",
      action: action || "Review the input data.",
      statusCode: 400,
      cause,
    });
  }
}

export class InternalServerError extends BaseError {
  constructor({ cause } = {}) {
    super({
      name: "InternalServerError",
      message: "An unexpected error occurred.",
      action: "Contact support.",
      statusCode: 500,
      cause,
    });
  }
}

try/catch que engole o erro

❌ Ruim: captura, loga e retorna null
async function findProductById(id) {
  try {
    const results = await db.query(id);

    if (results.rowCount === 0) {
      throw "Product not found";
    }

    return results.rows[0];
  } catch (error) {
    console.log("Something went wrong"); // engole o erro
    return null;
  }
}
✅ Bom: propaga com contexto, trata no limite do sistema
async function findProductById(id) {
  try {
    const product = await productRepository.findById(id);

    if (!product) {
      throw new NotFoundError({
        message: `Product ${id} not found.`,
        action: "Check if the product ID is correct.",
      });
    }

    return product;
  } catch (error) {
    if (error instanceof NotFoundError) throw error;

    throw new InternalServerError({ cause: error });
  }
}

Exceção como controle de fluxo

❌ Ruim: try/catch controlando lógica de negócio normal
function getUser(id) {
  try {
    return userMap[id]; // undefined não é uma exceção
  } catch {
    return null;
  }
}
✅ Bom: verificação explícita, sem exceção para fluxo normal
function getUser(id) {
  const user = userMap[id] ?? null;
  return user;
}

Quando usar try/catch

UseNão use
I/O externo (DB, rede, arquivo)Para encadear chamadas que já propagam erros
Limite do sistema (controller HTTP)Para logar e ignorar: mascara problemas
Para mapear erro técnico → erro de negócioQuando o erro já será tratado em camada superior

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.