Handle Errors
Define Custom Tagged Errors
import { Schema } from "effect"
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
path: Schema.String
}) {}
class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {
reason: Schema.String
}) {}
class RateLimited extends Schema.TaggedError<RateLimited>()("RateLimited", {
retryAfter: Schema.Number
}) {}
Schema.TaggedError creates a tagged error class with typed fields. The tag (first string argument) is what you use with catchTag. The schema fields are validated at construction.
You can also use Data.TaggedError for errors without schema validation:
import { Data } from "effect"
class SimpleError extends Data.TaggedError<SimpleError>()("SimpleError", {
message: Data.String
}) {}
Use Schema.TaggedError when you want runtime validation on error fields. Use Data.TaggedError for lightweight errors where you control construction.
Catch a Specific Error by Tag
import { Effect, Schema } from "effect"
class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
path: Schema.String
}) {}
class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {
reason: Schema.String
}) {}
declare const loadResource: Effect.Effect<string, NotFound | Unauthorized, never>
const program = loadResource.pipe(
Effect.catchTag("NotFound", (err) =>
Effect.succeed(`Resource not found at ${err.path}, using default`)
)
)
catchTag intercepts one error type by its tag string. The error parameter is fully typed, so err.path is a string. Other error types pass through unchanged.
Catch Multiple Error Tags
const program = loadResource.pipe(
Effect.catchTags({
NotFound: (err) => Effect.succeed(`Not found: ${err.path}`),
Unauthorized: (err) => Effect.succeed(`Unauthorized: ${err.reason}`)
})
)
catchTags handles several error types in one call. Each handler receives the typed error for its tag.
Tip: You can also pass an array of tags to
catchTagto share one handler across multiple error types:Effect.catchTag(["NotFound", "Unauthorized"], (err) => Effect.succeed("recovered")).
Catch All Errors
const program = loadResource.pipe(
Effect.catch((error) => Effect.succeed("recovered with fallback"))
)
Effect.catch intercepts any error in the error channel. Use this as a last resort when you don’t need to branch on error type.
Gotcha:
Effect.catchcatches all errors including defects (unexpected runtime errors). If you only want to catch expected domain errors, usecatchTagorcatchTagsinstead.
Recover with a Fallback
import { Effect, Schema } from "effect"
class FetchError extends Schema.TaggedError<FetchError>()("FetchError", {
url: Schema.String
}) {}
const fetchWithFallback = (url: string, fallback: string) =>
Effect.tryPromise({
try: () => fetch(url).then((r) => r.text()),
catch: () => new FetchError({ url })
}).pipe(
Effect.catchTag("FetchError", () => Effect.succeed(fallback))
)
The fallback effect can be any Effect, including one that itself fails. Compose catchTag with Effect.orElse for multi-level fallbacks.
Accumulate Errors with mode: "result"
import { Effect, Schema } from "effect"
class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
field: Schema.String,
message: Schema.String
}) {}
const validateName = (name: string) =>
name.length > 0
? Effect.succeed(name)
: new ValidationError({ field: "name", message: "Name is required" })
const validateEmail = (email: string) =>
email.includes("@")
? Effect.succeed(email)
: new ValidationError({ field: "email", message: "Invalid email" })
const validateAge = (age: number) =>
age >= 18
? Effect.succeed(age)
: new ValidationError({ field: "age", message: "Must be 18+" })
const validateForm = (input: { name: string; email: string; age: number }) =>
Effect.gen(function*() {
const results = yield* Effect.all(
[
validateName(input.name),
validateEmail(input.email),
validateAge(input.age)
],
{ mode: "result" }
)
const errors = results.filter((r) => r._tag === "Failure")
if (errors.length > 0) {
return errors.map((r) => (r as any).cause.error)
}
return "All valid"
})
Effect.runPromise(
validateForm({ name: "", email: "bad", age: 15 })
).then(console.log)
// [{ field: "name", message: "Name is required" },
// { field: "email", message: "Invalid email" },
// { field: "age", message: "Must be 18+" }]
Effect.all with { mode: "result" } runs all effects and collects results as Exit values instead of short-circuiting on the first failure. This lets you gather all validation errors at once.
Gotcha: Without
{ mode: "result" },Effect.allstops at the first failure, so you only see one error at a time.
Complete Example: API Error Recovery
import { Effect, Schema, Console } from "effect"
class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
url: Schema.String
}) {}
class ServerError extends Schema.TaggedError<ServerError>()("ServerError", {
status: Schema.Number,
url: Schema.String
}) {}
const apiCall = (url: string) =>
Effect.tryPromise({
try: () => fetch(url).then((r) => {
if (r.status >= 500) throw new ServerError({ status: r.status, url })
if (!r.ok) throw new Error(`${r.status}`)
return r.json()
}),
catch: (e) =>
e instanceof ServerError
? e
: new NetworkError({ url })
})
const robustFetch = (url: string) =>
apiCall(url).pipe(
Effect.catchTags({
NetworkError: (err) =>
Effect.gen(function*() {
yield* Console.log(`Network error on ${err.url}, using cached data`)
return { cached: true }
}),
ServerError: (err) =>
Effect.gen(function*() {
yield* Console.log(`Server returned ${err.status} for ${err.url}`)
return yield* new ServerError({ status: err.status, url: err.url })
})
})
)
Effect.runPromise(robustFetch("https://api.example.com/data"))
Next Steps
- Define Services - Structure your app with Context.Service
- Retry and Schedule - Automatic retries with backoff