Files
atelier/examples/bad/silent-error.md
T
2026-08-05 00:33:28 +00:00

4.0 KiB

Bad Example: Silent Error

An error-handling pattern that violates Atelier principles. Each violation is cited.

The Code

async function getUser(id: string): Promise<User | null> {
  try {
    const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
    return user;
  } catch (e) {
    return null;
  }
}

async function processOrder(orderId: string): Promise<void> {
  const order = await getOrder(orderId);
  if (!order) {
    return;  // silently do nothing
  }
  // ... process
}

// Usage in a route
router.get('/users/:id', async (req, res) => {
  const user = await getUser(req.params.id);
  if (!user) {
    res.status(404).json({ error: 'Not found' });
  } else {
    res.json({ data: user });
  }
});

Violations

Errors P2 Fail Loudly (Errors)

  • catch (e) { return null } swallows the error. The caller cannot distinguish "user not found" from "database down."
  • A database outage returns 404s. The operator never knows. Silent failure.
  • Fix: Catch and re-throw with context, or return a typed error (Result<User, Error>). Never null for "an error happened."

Errors P3 Fail Specifically (Errors)

  • return null is the least specific response. It could mean: not found, db error, network error, permission error.
  • The caller's if (!user) cannot distinguish these. The 404 is a lie if the real cause was a 500.
  • Fix: Return Result or throw. The error type/code carries the specificity.

Errors P1 Errors are Data (Errors)

  • null is not data. It is the absence of data. Conflating "error" with "absence" loses information.
  • The error (a database failure) was data; it was thrown away and replaced with null.
  • Fix: Errors are values. Return the error value, not a sentinel absence.

Errors P4 Preserve Context (Errors)

  • The catch block discards e. The stack trace, the error message, the cause — all gone.
  • The log has no record. The operator cannot debug.
  • Fix: Log the error with context. Wrap and re-throw: throw new Error('getUser failed', { cause: e }).

Errors P9 Errors are Logged (Errors)

  • The error is not logged. The handling (return null) is the entire response. The log is missing.
  • An error that is not logged is an error that cannot be investigated.
  • Fix: logger.error({ err: e, userId: id }) before returning/rethrowing.

Errors P10 Errors Don't Lie (Errors)

  • return null claims "no user" when the truth may be "database down." The function lies.
  • The 404 response claims "not found" when the truth may be "internal error." The API lies.
  • Fix: The response status must match the actual condition. 500 for server errors, 404 for not found.

Observability P2 Correlation, P3 Context (Observability)

  • No request_id. No correlation across services.
  • No context in the (missing) log. "What was the user doing?" is unanswerable.
  • Fix: Propagate request_id. Log with path, method, user_id.

Security P8 Fail Securely (Security)

  • The silent failure is fail-open in disguise. If getUser fails due to an authz check throwing, the catch returns null.
  • The caller treats null as "not found" and may proceed, or may 404. Either way, the security failure is hidden.
  • Fix: Distinguish "not found" (404) from "authz error" (403) from "db error" (500). Never collapse them into null.

What This Example Reveals

The silent error is the most common and most damaging anti-pattern. It violates Errors P2 (Fail Loudly), P3 (Fail Specifically), P1 (Errors are Data), P4 (Preserve Context), P9 (Errors are Logged), P10 (Errors Don't Lie) — six of ten error principles in one catch block.

The downstream effects:

  • Operators cannot debug (no log, no context).
  • Users see wrong errors (404 for a 500).
  • Security failures hide (authz error becomes "not found").
  • The system appears healthy when it is not (no metrics, no logs).

The fix is always the same: never swallow an error. Log it, wrap it, rethrow it, or return it as a typed value. Never return null for "something went wrong."