Ali

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

  1. Tag every error. Data.TaggedError gives you a _tag to match on.
  2. Fail at the edge, recover in the middle. Parse inputs early, decide policy late.
  3. 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.