You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Accept `cause: unknown` (the raw caught error) and call `extractErrorMessage` inside the factory's message template — not at the call site. This keeps call sites clean (`{ cause: error }`) and centralizes message extraction where the message is composed. The anti-pattern to avoid is **string literal unions** (`'a' | 'b' | 'c'`) acting as sub-discriminants. See [Anti-Pattern: Discriminated Union Inputs in Error Factories](#anti-pattern-discriminated-union-inputs-in-error-factories) below.
92
+
93
+
#### Why `extractErrorMessage` belongs inside the constructor
94
+
95
+
**Anti-pattern — transforming at the call site:**
96
+
```typescript
97
+
// Every call site must remember to call extractErrorMessage
-**The constructor owns the message template — it should also own the transformation.**`extractErrorMessage` is a message-formatting concern; it belongs where the message string is assembled, not scattered across every call site.
112
+
-**Raw `cause: unknown` preserves the original error for programmatic access.** Downstream code can inspect, log, or re-wrap the actual error object — not just a lossy string summary.
Whena`defineErrors`variant's input contains a string literal union field — such as `reason: 'a' | 'b' | 'c'` or `operation: 'read' | 'write'` — that field is acting as a **sub-discriminant**, duplicating the role that variant names already serve. This is a code smell. Split into separate variants instead.
// Consumers are forced into two levels of narrowing
193
+
if (error.name === 'InvalidAccelerator') {
194
+
if (error.reason === 'invalid_format') {
195
+
// now we finally know what happened
196
+
}
197
+
}
198
+
```
199
+
200
+
**2.Dishonesttypes.**Fieldsstartbecomingoptionalbecause"some reasons don't use that field". Thetypesays `accelerator?: string`, but the real contract is "required when reason is `'invalid_format'`, meaningless otherwise." TypeScript cannot express this conditional relationship within a single variant, so the type lies about the shape of the data.
201
+
202
+
**3.Messagelookuptables.**A`const messages = { ... }`objectinsidethefactoryfunction is a strong signal that the variant is doing too much. Each lookup entry is really its own error with its own message template — it should be its own variant.
203
+
204
+
### Example
205
+
206
+
Before — asinglevariantwithastringliteralunionactingasasub-discriminant:
Copy file name to clipboardExpand all lines: NAMING_CONVENTION.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -115,5 +115,6 @@ if (isErr(result)) {
115
115
3.**Be specific**: `Authentication` vs generic `Service`
116
116
4.**Group by domain**: `UserError.Validation`, `HttpError.Timeout`, `DbError.Connection`
117
117
5.**Namespace as the "Error" suffix**: The variable name carries the "Error" suffix (`UserError`), individual variants do not
118
+
6.**No string literal unions as sub-discriminants**: If a variant input has a field like `reason: 'a' | 'b' | 'c'`, each literal should be its own variant instead. The variant name is the discriminant — fields should carry data, not act as a second tag to switch on
118
119
119
120
This convention provides clear semantics and helps developers understand the distinction between the error data itself and the data structures that contain it.
0 commit comments