Effect provides a type-safe approach to error handling where errors are tracked in the type system. This makes it impossible to forget to handle errors and enables powerful recovery patterns.
Why Typed Errors?
In traditional TypeScript, errors can be thrown from anywhere and aren’t tracked in types:
// What errors can this throw? You have to read the code or docs!
function parsePort ( input : string ) : number {
const port = parseInt ( input )
if ( isNaN ( port )) throw new Error ( "Invalid port" )
if ( port < 1024 ) throw new Error ( "Reserved port" )
return port
}
With Effect, errors are explicit in the type signature:
import { Effect } from "effect"
function parsePort ( input : string ) : Effect . Effect < number , ParseError | ReservedPortError > {
// Errors are tracked in the type!
}
Effect’s type system ensures you handle all possible errors, preventing runtime surprises.
Defining Errors with Schema.TaggedErrorClass
The standard way to define errors in Effect is using Schema.TaggedErrorClass:
import { Schema } from "effect"
// Define custom errors using Schema.TaggedErrorClass
export class ParseError extends Schema . TaggedErrorClass < ParseError >()( "ParseError" , {
input: Schema . String ,
message: Schema . String
}) {}
export class ReservedPortError extends Schema . TaggedErrorClass < ReservedPortError >()( "ReservedPortError" , {
port: Schema . Number
}) {}
Tagged errors have a _tag field that identifies the error type. This enables type-safe pattern matching.
Why Use TaggedErrorClass?
Tagged errors can carry structured data about the failure: class ValidationError extends Schema . TaggedErrorClass < ValidationError >()( "ValidationError" , {
field: Schema . String ,
value: Schema . Unknown ,
rule: Schema . String
}) {}
// Usage
new ValidationError ({
field: "email" ,
value: "invalid" ,
rule: "must be valid email"
})
Type-Safe Pattern Matching
The _tag field enables exhaustive pattern matching: effect . pipe (
Effect . catchTag ( "ValidationError" , ( error ) => {
// error is narrowed to ValidationError
console . log ( `Field ${ error . field } failed: ${ error . rule } ` )
return Effect . succeed ( defaultValue )
})
)
Tagged errors are automatically validated against their schema, ensuring data integrity.
Raising Errors
Raise errors by yielding them in Effect.gen or Effect.fn:
import { Effect , Schema } from "effect"
class InvalidInput extends Schema . TaggedErrorClass < InvalidInput >()( "InvalidInput" , {
message: Schema . String
}) {}
const program = Effect . gen ( function* () {
const input = yield * getInput ()
if ( ! isValid ( input )) {
// Always use `return yield*` when raising errors
return yield * new InvalidInput ({ message: "Input validation failed" })
}
return processInput ( input )
})
Always use return yield* when raising errors in Effect.gen or Effect.fn. This ensures TypeScript understands the control flow.
Catching Errors
Effect provides several ways to catch and recover from errors.
Effect.catchTag - Catch a Specific Error
Use Effect.catchTag to handle a single error type:
import { Effect , Schema } from "effect"
class ParseError extends Schema . TaggedErrorClass < ParseError >()( "ParseError" , {
input: Schema . String ,
message: Schema . String
}) {}
class ReservedPortError extends Schema . TaggedErrorClass < ReservedPortError >()( "ReservedPortError" , {
port: Schema . Number
}) {}
declare const loadPort : ( input : string ) => Effect . Effect < number , ParseError | ReservedPortError >
const withRecovery = loadPort ( "80" ). pipe (
// Catch a specific error with Effect.catchTag
Effect . catchTag ( "ReservedPortError" , ( error ) => {
// error is narrowed to ReservedPortError
console . log ( `Port ${ error . port } is reserved, using default` )
return Effect . succeed ( 3000 )
})
)
// Type: Effect<number, ParseError, never>
// ReservedPortError is handled!
Effect.catchTag - Catch Multiple Errors
You can also pass an array of tags to catch multiple errors with the same handler:
const recovered = loadPort ( "80" ). pipe (
// Catch multiple errors with Effect.catchTag
Effect . catchTag ([ "ParseError" , "ReservedPortError" ], ( error ) => {
// error is narrowed to ParseError | ReservedPortError
return Effect . succeed ( 3000 )
})
)
// Type: Effect<number, never, never>
// Both errors are handled!
Use Effect.catchTags to handle different errors with different logic:
import { Effect , Schema } from "effect"
class ValidationError extends Schema . TaggedErrorClass < ValidationError >()( "ValidationError" , {
message: Schema . String
}) {}
class NetworkError extends Schema . TaggedErrorClass < NetworkError >()( "NetworkError" , {
statusCode: Schema . Number
}) {}
declare const fetchUser : ( id : string ) => Effect . Effect < string , ValidationError | NetworkError >
const userOrFallback = fetchUser ( "123" ). pipe (
Effect . catchTags ({
ValidationError : ( error ) =>
Effect . succeed ( `Validation failed: ${ error . message } ` ),
NetworkError : ( error ) =>
Effect . succeed ( `Network request failed with status ${ error . statusCode } ` )
})
)
// Type: Effect<string, never, never>
Use Effect.catchTags when you need different recovery strategies for different error types.
Effect.catch - Catch All Errors
Use Effect.catch to handle any error:
const withFinalFallback = loadPort ( "invalid" ). pipe (
Effect . catchTag ( "ReservedPortError" , () => Effect . succeed ( 3000 )),
// Catch all remaining errors with Effect.catch
Effect . catch (( error ) => {
// error could be any remaining error type
console . log ( "Unexpected error:" , error )
return Effect . succeed ( 3000 )
})
)
// Type: Effect<number, never, never>
Effect.catch catches all errors, including any unhandled error types. Use it as a final fallback.
Effect.mapError
Transform errors into different error types:
import { Effect , Schema } from "effect"
class DatabaseError extends Schema . TaggedErrorClass < DatabaseError >()( "DatabaseError" , {
cause: Schema . Defect
}) {}
class UserRepositoryError extends Schema . TaggedErrorClass < UserRepositoryError >()( "UserRepositoryError" , {
reason: DatabaseError
}) {}
declare const queryDatabase : Effect . Effect < User , DatabaseError >
const findUser = queryDatabase . pipe (
Effect . mapError (( dbError ) => new UserRepositoryError ({ reason: dbError }))
)
// Type: Effect<User, UserRepositoryError, never>
Use Effect.mapError to wrap lower-level errors in domain-specific error types.
Error Recovery Patterns
Provide Default Values
const userOrDefault = fetchUser ( "123" ). pipe (
Effect . catchTag ( "NotFound" , () => Effect . succeed ({ id: "123" , name: "Guest" }))
)
Retry on Specific Errors
import { Schedule } from "effect"
const withRetry = fetchUser ( "123" ). pipe (
Effect . retry ({
schedule: Schedule . exponential ( "100 millis" ). pipe (
Schedule . upTo ( "5 seconds" )
),
while : ( error ) => error . _tag === "NetworkError"
})
)
Fallback to Alternative
const userFromCacheOrDb = fetchFromCache ( "123" ). pipe (
Effect . catchTag ( "CacheMiss" , () => fetchFromDatabase ( "123" ))
)
Log and Rethrow
const withLogging = fetchUser ( "123" ). pipe (
Effect . tapError (( error ) =>
Effect . logError ( "Failed to fetch user:" , error )
)
)
// Error is logged but still propagates
Error Channels
Effect has two separate channels for errors:
Expected Errors (E channel)
Expected errors are tracked in the type signature and must be handled: const program : Effect . Effect < number , ParseError , never >
These represent recoverable failures that are part of your domain logic.
Unexpected Errors (Defects)
Defects are unexpected errors (like bugs) that aren’t tracked in types: Effect . die ( new Error ( "This should never happen" ))
Use defects for programming errors, not recoverable failures.
Don’t use defects for expected error conditions. Use tagged errors instead so they’re tracked in types.
Combining Error Handling
Chain error handlers for sophisticated recovery:
const robust = fetchUser ( "123" ). pipe (
// Try to recover from specific errors
Effect . catchTag ( "NetworkError" , () => fetchFromCache ( "123" )),
Effect . catchTag ( "CacheMiss" , () => Effect . succeed ( guestUser )),
// Log any remaining errors
Effect . tapError (( error ) => Effect . logError ( "Unexpected error:" , error )),
// Final fallback
Effect . catch (() => Effect . succeed ( guestUser ))
)
Best Practices
Use Schema.TaggedErrorClass for all domain errors
Tagged errors enable type-safe pattern matching and carry structured data
Be specific with error types
Create separate error classes for different failure modes
Handle errors close to where they occur
Catch and transform errors at the right abstraction level
Use Effect.mapError to wrap errors
Wrap lower-level errors in domain-specific errors as you move up layers
Always use return yield* when raising errors
This ensures TypeScript understands the control flow
Design your error hierarchy to match your domain. Each service should have its own error types.
Common Patterns
Wrapping External Errors
import { Effect , Schema } from "effect"
class HttpError extends Schema . TaggedErrorClass < HttpError >()( "HttpError" , {
statusCode: Schema . Number ,
cause: Schema . Defect
}) {}
const fetchData = ( url : string ) =>
Effect . tryPromise ({
try : () => fetch ( url ). then ( r => r . json ()),
catch : ( cause ) => new HttpError ({
statusCode: ( cause as any ). status ?? 500 ,
cause
})
})
Error Reason Patterns
import { Schema } from "effect"
class UserError extends Schema . TaggedErrorClass < UserError >()( "UserError" , {
reason: Schema . TaggedUnion (
Schema . TaggedStruct ( "NotFound" , { userId: Schema . String }),
Schema . TaggedStruct ( "Suspended" , { userId: Schema . String , until: Schema . Date }),
Schema . TaggedStruct ( "InvalidCredentials" , {})
)
}) {}
// Handle based on reason
effect . pipe (
Effect . catchTag ( "UserError" , ( error ) => {
switch ( error . reason . _tag ) {
case "NotFound" :
return handleNotFound ( error . reason . userId )
case "Suspended" :
return handleSuspended ( error . reason . userId , error . reason . until )
case "InvalidCredentials" :
return handleInvalidCredentials ()
}
})
)
Next Steps
Learn about Services
Understand how errors propagate through services at Services
Master Resource Management
See how errors interact with resources in Resources