Prerequisites
npm install effect
Pattern 1: Tagged Errors
Define type-safe errors withData.TaggedError:
tagged-errors.ts
import { Data, Effect } from "effect"
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
readonly userId: string
}> {
toString(): string {
return `User with ID '${this.userId}' not found`
}
}
class DatabaseError extends Data.TaggedError("DatabaseError")<{
readonly cause: unknown
}> {
toString(): string {
return `Database error: ${this.cause}`
}
}
class ValidationError extends Data.TaggedError("ValidationError")<{
readonly field: string
readonly message: string
}> {}
// Use tagged errors in your code
const findUser = (userId: string) =>
Effect.gen(function*() {
if (userId.length === 0) {
return yield* Effect.fail(new ValidationError({
field: "userId",
message: "User ID cannot be empty"
}))
}
// Simulate database lookup
const user = null // Database query result
if (user === null) {
return yield* Effect.fail(new UserNotFoundError({ userId }))
}
return user
})
Pattern 2: Catching Specific Errors
Handle different error types withcatchTag:
catch-specific.ts
import { Console, Effect } from "effect"
const program = findUser("123").pipe(
Effect.catchTag("UserNotFoundError", (error) =>
Console.log(`User not found: ${error.userId}`).pipe(
Effect.as(null)
)
),
Effect.catchTag("ValidationError", (error) =>
Console.log(`Validation failed: ${error.message}`).pipe(
Effect.as(null)
)
),
Effect.catchTag("DatabaseError", (error) =>
Console.log(`Database error occurred`).pipe(
Effect.flatMap(() => Effect.fail(error)) // Re-throw
)
)
)
Pattern 3: Error Recovery with Fallbacks
Provide fallback values when errors occur:fallbacks.ts
import { Effect } from "effect"
// Return default value on any error
const withDefault = findUser("123").pipe(
Effect.catchAll(() => Effect.succeed({ id: "guest", name: "Guest User" }))
)
// Use orElse to try alternative operations
const findUserWithFallback = (userId: string) =>
findUser(userId).pipe(
Effect.orElse(() => findUserInCache(userId)),
Effect.orElse(() => createGuestUser())
)
// Retry with fallback
const withRetryAndFallback = findUser("123").pipe(
Effect.retry({ times: 3 }),
Effect.catchAll(() => Effect.succeed(null))
)
Pattern 4: Error Transformation
Convert errors to different types:transform-errors.ts
import { Effect } from "effect"
class ApiError extends Data.TaggedError("ApiError")<{
readonly statusCode: number
readonly message: string
}> {}
// Map domain errors to API errors
const toApiError = (userId: string) =>
findUser(userId).pipe(
Effect.mapError((error) => {
switch (error._tag) {
case "UserNotFoundError":
return new ApiError({ statusCode: 404, message: error.toString() })
case "ValidationError":
return new ApiError({ statusCode: 400, message: error.message })
case "DatabaseError":
return new ApiError({ statusCode: 500, message: "Internal server error" })
}
})
)
Pattern 5: Validation with Schema
Validate data with Effect Schema:schema-validation.ts
import { Effect } from "effect"
import * as Schema from "effect/Schema"
const UserSchema = Schema.Struct({
id: Schema.String.pipe(
Schema.minLength(1, { message: () => "ID cannot be empty" })
),
name: Schema.String.pipe(
Schema.minLength(2, { message: () => "Name must be at least 2 characters" })
),
email: Schema.String.pipe(
Schema.pattern(/^[^@]+@[^@]+\.[^@]+$/, {
message: () => "Invalid email format"
})
),
age: Schema.Number.pipe(
Schema.int({ message: () => "Age must be an integer" }),
Schema.greaterThanOrEqualTo(0, { message: () => "Age cannot be negative" }),
Schema.lessThan(150, { message: () => "Age must be realistic" })
)
})
const validateUser = (data: unknown) =>
Effect.gen(function*() {
const user = yield* Schema.decodeUnknown(UserSchema)(data)
return user
}).pipe(
Effect.catchAll((error) =>
Effect.gen(function*() {
yield* Console.log("Validation failed:")
yield* Console.log(error)
return yield* Effect.fail(new ValidationError({
field: "user",
message: "Invalid user data"
}))
})
)
)
Pattern 6: Resource Safety
Ensure cleanup happens even when errors occur:resource-safety.ts
import { Effect, Scope } from "effect"
const openFile = (path: string) =>
Effect.acquireRelease(
Effect.gen(function*() {
yield* Console.log(`Opening file: ${path}`)
return { path, handle: 123 }
}),
(file) => Console.log(`Closing file: ${file.path}`)
)
const processFile = (path: string) =>
Effect.gen(function*() {
const file = yield* openFile(path)
// Even if this fails, file will be closed
yield* Effect.fail(new Error("Processing failed"))
return file
}).pipe(
Effect.scoped
)
// File is guaranteed to be closed
processFile("data.txt").pipe(
Effect.catchAll(() => Console.log("Handled error, file was closed")),
Effect.runPromise
)
Pattern 7: Collecting Multiple Errors
Validate multiple fields and collect all errors:collect-errors.ts
import { Effect, Array } from "effect"
const validateField = (field: string, value: string) =>
value.length > 0
? Effect.succeed(value)
: Effect.fail(new ValidationError({ field, message: `${field} is required` }))
const validateForm = (data: Record<string, string>) =>
Effect.gen(function*() {
const results = yield* Effect.all(
[
validateField("name", data.name),
validateField("email", data.email),
validateField("password", data.password)
],
{ mode: "validate" } // Collect all errors instead of failing fast
)
return results
}).pipe(
Effect.catchAll((errors) =>
Effect.gen(function*() {
yield* Console.log("Validation errors:")
Array.forEach(errors, (error) => Console.log(`- ${error.message}`))
return yield* Effect.fail(errors)
})
)
)
Pattern 8: Defect vs Expected Errors
Distinguish between recoverable errors and bugs:defects.ts
import { Effect } from "effect"
// Expected errors - part of the domain
const expectedError = Effect.fail(new UserNotFoundError({ userId: "123" }))
// Defects - programming bugs
const defect = Effect.die(new Error("This should never happen"))
// Catch only expected errors
expectedError.pipe(
Effect.catchAll(() => Console.log("Handled expected error"))
)
// Catch defects (usually only for logging)
defect.pipe(
Effect.catchAllDefect((error) =>
Console.log(`Caught defect: ${error.message}`)
)
)
// Catch both errors and defects
const catchEverything = Effect.gen(function*() {
yield* someEffect
}).pipe(
Effect.catchAllCause((cause) =>
Console.log(`Something went wrong: ${cause}`)
)
)
Pattern 9: Error Context and Logging
Add context to errors for debugging:error-context.ts
import { Effect } from "effect"
const enrichedError = findUser("123").pipe(
Effect.tapError((error) =>
Console.log(`Failed to find user: ${error._tag}`)
),
Effect.annotateLogs({
operation: "findUser",
userId: "123",
timestamp: new Date().toISOString()
}),
Effect.withSpan("findUser", { attributes: { userId: "123" } })
)
Pattern 10: Production Error Handling
Complete error handling for production:production-errors.ts
import { Effect, Console } from "effect"
const productionHandler = <A, E>(effect: Effect.Effect<A, E>) =>
effect.pipe(
// Retry transient errors
Effect.retry({
times: 3,
schedule: Schedule.exponential("100 millis")
}),
// Catch expected errors
Effect.catchTag("UserNotFoundError", (error) =>
Console.log(`User not found: ${error.userId}`).pipe(
Effect.as(null)
)
),
Effect.catchTag("DatabaseError", (error) =>
Console.log("Database error, using cache").pipe(
Effect.flatMap(() => fallbackToCache())
)
),
// Log unexpected errors
Effect.tapErrorCause((cause) =>
Effect.logError(`Unexpected error: ${cause}`)
),
// Provide safe default
Effect.catchAll(() => Effect.succeed(null))
)
Testing Error Scenarios
Test error handling:error-testing.ts
import { Effect } from "effect"
import { describe, it, assert } from "@effect/vitest"
describe("Error handling", () => {
it.effect("should handle UserNotFoundError", () =>
Effect.gen(function*() {
const result = yield* findUser("invalid").pipe(
Effect.catchTag("UserNotFoundError", () => Effect.succeed(null)),
Effect.flip
)
assert.strictEqual(result, null)
})
)
it.effect("should retry on DatabaseError", () =>
Effect.gen(function*() {
let attempts = 0
const flakyEffect = Effect.gen(function*() {
attempts++
if (attempts < 3) {
return yield* Effect.fail(new DatabaseError({ cause: "timeout" }))
}
return "success"
})
const result = yield* flakyEffect.pipe(
Effect.retry({ times: 3 })
)
assert.strictEqual(result, "success")
assert.strictEqual(attempts, 3)
})
)
})
Next Steps
Building HTTP Server
Apply error handling in web servers
Database Integration
Handle database errors
CLI Applications
Error handling in CLI apps
Effect API Reference
Full Effect error handling API