Errors are values, not surprises
· 1 min read · typescript, effect
In most TypeScript code, the signature (id: string) => Promise<User> is a polite lie.
It might throw a network error, a JSON parse error, a 404, or something nobody anticipated.
The type says none of that.
Put the failure in the type
With Effect, the same function becomes:
const getUser = (id: string): Effect.Effect<User, UserNotFound | NetworkError> =>
// ...
Now the compiler knows. And so does every caller.
const program = getUser("42").pipe(
Effect.catchTag("UserNotFound", () => Effect.succeed(guest))
)
// program: Effect<User, NetworkError>
Handle one case and it disappears from the error channel. Forget one and the type reminds you.
Three rules I follow
- Tag every error.
Data.TaggedErrorgives you a_tagto match on. - Fail at the edge, recover in the middle. Parse inputs early, decide policy late.
- Never swallow. If you catch it, log it or transform it — don't make it vanish.
| Approach | Visible in types | Composable | Exhaustive |
|---|---|---|---|
throw |
✗ | ✗ | ✗ |
Result<T, E> |
✓ | ~ | ✓ |
Effect<A, E> |
✓ | ✓ | ✓ |
Errors aren't exceptional. They're part of the domain. Treat them that way.