496303471d
---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.
88 lines
4.0 KiB
Markdown
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." |