Files
atelier/examples/bad/silent-error.md
T
Jon Chery 496303471d docs(milestone): complete v0.1 — initial framework
---ci---
project: atelier
phase: 7
milestone: v0.1
status: complete
phase_role: final
milestone_complete: true
requirements:
  covered: [ATELIER-01, ATELIER-02, ATELIER-03, ATELIER-04, ATELIER-05, ATELIER-06, ATELIER-07, ATELIER-08, ATELIER-09, ATELIER-10, ATELIER-11, ATELIER-12, ATELIER-13, ATELIER-14, ATELIER-15, ATELIER-16, ATELIER-17, ATELIER-18, ATELIER-19, ATELIER-20, ATELIER-21, ATELIER-22, ATELIER-23, ATELIER-24, ATELIER-25, ATELIER-26, ATELIER-27, ATELIER-28, ATELIER-29, ATELIER-30, ATELIER-31, ATELIER-32, ATELIER-33, ATELIER-34, ATELIER-35]
  partial: []
ship:
  milestone: v0.1
  type: NFR
  tag: v0.0.7
  merge: milestone/v0.1-atelier -> main
  release: https://git.cloudinit.dev/cloudinit-bot/atelier/releases/tag/v0.0.7
---/ci---

Milestone v0.1 — Initial Framework (NFR, complete).
8 core principles (C1-C8), 11 domains, 110 domain principles, 27 derived docs, 4 good + 3 bad examples, 4 language docs, full matrix, 3 review docs.
All 35 requirements covered. 7 patches (v0.0.0 pre-execution through v0.0.7 final). v0.0.7 IS the v0.1.0 milestone release.
2026-08-05 00:36:55 +00:00

88 lines
4.0 KiB
Markdown

# Bad Example: Silent Error
> An error-handling pattern that violates Atelier principles. Each violation is cited.
## The Code
```typescript
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."