4.0 KiB
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>). Nevernullfor "an error happened."
Errors P3 Fail Specifically (Errors)
return nullis 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
Resultor throw. The error type/code carries the specificity.
Errors P1 Errors are Data (Errors)
nullis 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 nullclaims "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
getUserfails due to an authz check throwing, the catch returnsnull. - The caller treats
nullas "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."