Error Categories in Effect

Last updated:

effect


Errors in Effect often take the form of TaggedErrors which extend the global Error class, have a unique string _tag and additional custom properties:

class
class FooError
FooError
extends
import S
S
.
const TaggedError: <FooError>(identifier?: string) => <Tag, Fields>(tag: Tag, fieldsOr: Fields | HasFields<Fields>, annotations?: ClassAnnotations<FooError, { [K in keyof S.Struct<Fields extends S.Struct.Fields>.Type<...>]: S.Struct.Type<...>[K]; }> | undefined) => S.TaggedErrorClass<...>

@example

import { Schema } from "effect"
class MyError extends Schema.TaggedError<MyError>("MyError")(
"MyError",
{
module: Schema.String,
method: Schema.String,
description: Schema.String
}
) {
get message(): string {
return `${this.module}.${this.method}: ${this.description}`
}
}

@since3.10.0

TaggedError
<
class FooError
FooError
>()("FooError", {
bar: typeof S.String
bar
:
import S
S
.
class String
export String

@since3.10.0

String
,
}) {}
const
const foo: Effect.Effect<void, FooError, never>
foo
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const gen: <YieldWrap<Effect.Effect<never, FooError, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<never, FooError, never>>, void, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function* () {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
if (
var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.
Math.random(): number

Returns a pseudorandom number between 0 and 1.

random
() > 0.5) {
yield*
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const fail: <FooError>(error: FooError) => Effect.Effect<never, FooError, never>

Creates an Effect that represents a recoverable error.

When to Use

Use this function to explicitly signal an error in an Effect. The error will keep propagating unless it is handled. You can handle the error with functions like

catchAll

or

catchTag

.

Example (Creating a Failed Effect)

import { Effect } from "effect"
// ┌─── Effect<never, Error, never>
// ▼
const failure = Effect.fail(
new Error("Operation failed due to network error")
)

@seesucceed to create an effect that represents a successful value.

@since2.0.0

fail
(new
constructor FooError(props: {
readonly bar: string;
}, options?: S.MakeOptions): FooError
FooError
({
bar: string
bar
: "jfdsk" }));
}
});
const
const handled: Effect.Effect<void, never, never>
handled
=
const foo: Effect.Effect<void, FooError, never>
foo
.
Pipeable.pipe<Effect.Effect<void, FooError, never>, Effect.Effect<void, never, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, FooError, never>) => Effect.Effect<void, never, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchTag: <FooError, ["FooError"], void, never, never>(args_0: "FooError", f: (e: FooError) => Effect.Effect<void, never, never>) => <A, R>(self: Effect.Effect<A, FooError, R>) => Effect.Effect<...> (+3 overloads)

Catches and handles specific errors by their _tag field, which is used as a discriminator.

When to Use

catchTag is useful when your errors are tagged with a readonly _tag field that identifies the error type. You can use this function to handle specific error types by matching the _tag value. This allows for precise error handling, ensuring that only specific errors are caught and handled.

The error type must have a readonly _tag field to use catchTag. This field is used to identify and match errors.

Example (Handling Errors by Tag)

import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
// Only handle HttpError errors
Effect.catchTag("HttpError", (_HttpError) =>
Effect.succeed("Recovering from HttpError")
)
)

@seecatchTags for a version that allows you to handle multiple error types at once.

@since2.0.0

catchTag
("FooError", (
e: FooError
e
) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level.

Details

This function provides a simple way to log messages or error causes during the execution of your effects. By default, logs are recorded at the INFO level, but this can be adjusted using other logging utilities (Logger.withMinimumLogLevel). Multiple items, including Cause instances, can be logged in a single call. When logging Cause instances, detailed error information is included in the log output.

The log output includes useful metadata like the current timestamp, log level, and fiber ID, making it suitable for debugging and tracking purposes. This function does not interrupt or alter the effect's execution flow.

Example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
("recovered",
e: FooError
e
.
bar: string
bar
)),
);

The Problem

While handling errors one at a time by their _tag is very easy to do by default, this level of granularity can become difficult to manage as the number of errors in your application grows. Often, you simply want to group errors into “categories” which you can discriminate against.

Outside of Effect, inheritance is the common pattern by which to implement this. Errors in a category share a base class, and instanceof can be used to discriminate:

abstract class
class CategoryA
CategoryA
extends
var Error: ErrorConstructor
Error
{}
abstract class
class CategoryB
CategoryB
extends
var Error: ErrorConstructor
Error
{}
class
class FooError
FooError
extends
class CategoryA
CategoryA
{}
class
class BarError
BarError
extends
class CategoryA
CategoryA
{}
class
class BazError
BazError
extends
class CategoryB
CategoryB
{}
try {
// ...
} catch (
var error: unknown
error
) {
if (
var error: unknown
error
instanceof
class CategoryA
CategoryA
) {
// do A
} else if (
var error: unknown
error
instanceof
class CategoryB
CategoryB
) {
// do B
} else {
throw
var error: unknown
error
;
}
}

However this doesn’t work in Effect because javascript does not support multiple inheritance, and we are already extending the TaggedError class from Effect! We could try to make an intermediate class that extends TaggedError that we can again extend from, but there’s a small problem: the types for TaggedError are straight up wizardy. Good luck making a fully functional generic wrapper for that.

Good thing there is another way…

Mixins (and composition) to the rescuse

Mixins, despite their fancy name, are remarkably simple. They are basically functions that take in a class and return a new class. Surprisingly this is a pattern that has a whole page to it’s own in the official typescript docs.

type
type Class<T = {}> = new (...args: any[]) => T
Class
<
function (type parameter) T in type Class<T = {}>
T
= {}> = new (...
args: any[]
args
: any[]) =>
function (type parameter) T in type Class<T = {}>
T
;
const
const Mixin: <T extends Class<{
message: string;
}>>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: Mixin<any>.(Anonymous class);
} & T
Mixin
= <
function (type parameter) T in <T extends Class<{
message: string;
}>>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin<any>.(Anonymous class);
} & T
T
extends
type Class<T = {}> = new (...args: any[]) => T
Class
<{
message: string
message
: string }>>(
type Base: T extends Class<{ message: string; }>
Base
:
function (type parameter) T in <T extends Class<{
message: string;
}>>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin<any>.(Anonymous class);
} & T
T
) =>
class extends
type Base: T extends Class<{ message: string; }>
Base
{
get
function (Anonymous class).message2: string
message2
() {
return this.
message: string
message
+ this.
message: string
message
;
}
};
class
class MyError
MyError
extends
const Mixin: <ErrorConstructor>(Base: ErrorConstructor) => {
new (...args: any[]): Mixin<ErrorConstructor>.(Anonymous class);
prototype: Mixin<...>.(Anonymous class);
} & ErrorConstructor
Mixin
(
var Error: ErrorConstructor
Error
) {
MyError.message: string
message
= "hi";
}
const
const e: MyError
e
= new
constructor MyError(message?: string, options?: ErrorOptions): MyError (+1 overload)
MyError
();
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
const e: MyError
e
.
function (Anonymous class).message2: string
message2
); // "hihi"

There’s a couple cool things about this. First is that everything is fully typed- typescript can infer the type of the class returned from the mixin and merge it with any class you extend from it, and we can even provide constraints on what classes can be passed into the mixin.

Also because a mixin is just a function, we can do all sorts of cool functional things like composition. I’m sure you’ve heard of “composition over inheritance” before and this pattern is that saying to a T.

Additionally, because mixins are just functions, we can use our favorite utility from Effect: pipe

import {
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
} from "effect";
type
type Class<T = {}> = new (...args: any[]) => T
Class
<
function (type parameter) T in type Class<T = {}>
T
= {}> = new (...
args: any[]
args
: any[]) =>
function (type parameter) T in type Class<T = {}>
T
;
const
const Mixin1: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & T
Mixin1
= <
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & T
T
extends
type Class<T = {}> = new (...args: any[]) => T
Class
>(
type Base: T extends Class
Base
:
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & T
T
) =>
class extends
type Base: T extends Class
Base
{
function (Anonymous class).one: boolean
one
= true;
};
const
const Mixin2: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: Mixin2<any>.(Anonymous class);
} & T
Mixin2
= <
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin2<any>.(Anonymous class);
} & T
T
extends
type Class<T = {}> = new (...args: any[]) => T
Class
>(
type Base: T extends Class
Base
:
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: Mixin2<any>.(Anonymous class);
} & T
T
) =>
class extends
type Base: T extends Class
Base
{
function (Anonymous class).two: boolean
two
= true;
};
class
class MyError
MyError
extends
pipe<ErrorConstructor, {
new (...args: any[]): Mixin1<ErrorConstructor>.(Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & ErrorConstructor, {
...;
} & ... 1 more ... & ErrorConstructor>(a: ErrorConstructor, ab: (a: ErrorConstructor) => {
new (...args: any[]): Mixin1<ErrorConstructor>.(Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & ErrorConstructor, bc: (b: {
new (...args: any[]): Mixin1<ErrorConstructor>.(Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & ErrorConstructor) => {
...;
} & ... 1 more ... & ErrorConstructor): {
...;
} & ... 1 more ... & ErrorConstructor (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
(
var Error: ErrorConstructor
Error
,
const Mixin1: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: Mixin1<any>.(Anonymous class);
} & T
Mixin1
,
const Mixin2: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: Mixin2<any>.(Anonymous class);
} & T
Mixin2
) {}
const
const e: MyError
e
= new
constructor MyError(message?: string, options?: ErrorOptions): MyError (+1 overload)
MyError
();
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() andconsole.warn() that can be used to write to any Node.js stream.
  • A global console instance configured to write to process.stdout and process.stderr. The global console can be used without callingrequire('console').

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:

console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr

Example using the Console class:

const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err

@seesource

console
.
Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stdout with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout

See util.format() for more information.

@sincev0.1.100

log
(
const e: MyError
e
.
function (Anonymous class).one: boolean
one
&&
const e: MyError
e
.
function (Anonymous class).two: boolean
two
); // true

Back to categories

Ok so mixins are a cool pattern, but how does this help us with our error categorization problem?

Well we can start by making a unique interface for each category:

const
const CategoryA: typeof CategoryA
CategoryA
=
var Symbol: SymbolConstructor
Symbol
.
SymbolConstructor.for(key: string): symbol

Returns a Symbol object from the global symbol registry matching the given key if found. Otherwise, returns a new symbol with this key.

@paramkey key to search for.

for
("CategoryA");
interface
interface A
A
{
readonly [
const CategoryA: typeof CategoryA
CategoryA
]: true;
}
const
const CategoryB: typeof CategoryB
CategoryB
=
var Symbol: SymbolConstructor
Symbol
.
SymbolConstructor.for(key: string): symbol

Returns a Symbol object from the global symbol registry matching the given key if found. Otherwise, returns a new symbol with this key.

@paramkey key to search for.

for
("CategoryB");
interface
interface B
B
{
readonly [
const CategoryB: typeof CategoryB
CategoryB
]: true;
B.double(): number
double
(): number;
}

Then we can create a mixin which adds the necessary properties for the interface to the provided class:

const
const AMixin: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: AMixin<any>.(Anonymous class);
} & T
AMixin
= <
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: AMixin<any>.(Anonymous class);
} & T
T
extends
type Class<T = {}> = new (...args: any[]) => T
Class
>(
type Base: T extends Class
Base
:
function (type parameter) T in <T extends Class>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: AMixin<any>.(Anonymous class);
} & T
T
) =>
class extends
type Base: T extends Class
Base
implements
interface A
A
{
readonly [
const CategoryA: typeof CategoryA
CategoryA
] = true as
type const = true
const
;
};
const
const BMixin: <T extends Class<{
x: number;
}>>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: BMixin<any>.(Anonymous class);
} & T
BMixin
= <
function (type parameter) T in <T extends Class<{
x: number;
}>>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: BMixin<any>.(Anonymous class);
} & T
T
extends
type Class<T = {}> = new (...args: any[]) => T
Class
<{
x: number
x
: number }>>(
type Base: T extends Class<{ x: number; }>
Base
:
function (type parameter) T in <T extends Class<{
x: number;
}>>(Base: T): {
new (...args: any[]): (Anonymous class);
prototype: BMixin<any>.(Anonymous class);
} & T
T
) =>
class extends
type Base: T extends Class<{ x: number; }>
Base
implements
interface B
B
{
readonly [
const CategoryB: typeof CategoryB
CategoryB
] = true as
type const = true
const
;
function (Anonymous class).double(): number
double
() {
return 2 * this.
x: number
x
;
}
};

Now we can take our TaggedErrors from before, and just pipe them into the category mixins they belong to:

class
class FooError
FooError
extends
import Schema
Schema
.
const TaggedError: <FooError>(identifier?: string) => <Tag, Fields>(tag: Tag, fieldsOr: Fields | HasFields<Fields>, annotations?: ClassAnnotations<FooError, { [K in keyof Schema.Struct<Fields extends Schema.Struct.Fields>.Type<...>]: Schema.Struct.Type<...>[K]; }> | undefined) => Schema.TaggedErrorClass<...>

@example

import { Schema } from "effect"
class MyError extends Schema.TaggedError<MyError>("MyError")(
"MyError",
{
module: Schema.String,
method: Schema.String,
description: Schema.String
}
) {
get message(): string {
return `${this.module}.${this.method}: ${this.description}`
}
}

@since3.10.0

TaggedError
<
class FooError
FooError
>()("FooError", {}) {}
class
class BarError
BarError
extends
import Schema
Schema
.
const TaggedError: <BarError>(identifier?: string) => <Tag, Fields>(tag: Tag, fieldsOr: Fields | HasFields<Fields>, annotations?: ClassAnnotations<BarError, { [K in keyof Schema.Struct<Fields extends Schema.Struct.Fields>.Type<...>]: Schema.Struct.Type<...>[K]; }> | undefined) => Schema.TaggedErrorClass<...>

@example

import { Schema } from "effect"
class MyError extends Schema.TaggedError<MyError>("MyError")(
"MyError",
{
module: Schema.String,
method: Schema.String,
description: Schema.String
}
) {
get message(): string {
return `${this.module}.${this.method}: ${this.description}`
}
}

@since3.10.0

TaggedError
<
class BarError
BarError
>()("BarError", {
x: typeof Schema.Number
x
:
import Schema
Schema
.
class Number
export Number

@since3.10.0

Number
,
}).
Pipeable.pipe<Schema.TaggedErrorClass<BarError, "BarError", {
readonly _tag: Schema.tag<"BarError">;
} & {
x: typeof Schema.Number;
}>, {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...>>(this: Schema.TaggedErrorClass<...>, ab: (_: Schema.TaggedErrorClass<...>) => {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...>): {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...> (+21 overloads)
pipe
(
const AMixin: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: AMixin<any>.(Anonymous class);
} & T
AMixin
) {}
class
class BazError
BazError
extends
import Schema
Schema
.
const TaggedError: <BazError>(identifier?: string) => <Tag, Fields>(tag: Tag, fieldsOr: Fields | HasFields<Fields>, annotations?: ClassAnnotations<BazError, { [K in keyof Schema.Struct<Fields extends Schema.Struct.Fields>.Type<...>]: Schema.Struct.Type<...>[K]; }> | undefined) => Schema.TaggedErrorClass<...>

@example

import { Schema } from "effect"
class MyError extends Schema.TaggedError<MyError>("MyError")(
"MyError",
{
module: Schema.String,
method: Schema.String,
description: Schema.String
}
) {
get message(): string {
return `${this.module}.${this.method}: ${this.description}`
}
}

@since3.10.0

TaggedError
<
class BazError
BazError
>()("BazError", {
x: typeof Schema.Number
x
:
import Schema
Schema
.
class Number
export Number

@since3.10.0

Number
,
}).
Pipeable.pipe<Schema.TaggedErrorClass<BazError, "BazError", {
readonly _tag: Schema.tag<"BazError">;
} & {
x: typeof Schema.Number;
}>, {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...>, {
...;
} & ... 1 more ... & Schema.TaggedErrorClass<...>>(this: Schema.TaggedErrorClass<...>, ab: (_: Schema.TaggedErrorClass<...>) => {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...>, bc: (_: {
new (...args: any[]): AMixin<...>.(Anonymous class);
prototype: AMixin<...>.(Anonymous class);
} & Schema.TaggedErrorClass<...>) => {
...;
} & ... 1 more ... & Schema.TaggedErrorClass<...>): {
...;
} & ... 1 more ... & Schema.TaggedErrorClass<...> (+21 overloads)
pipe
(
const AMixin: <T extends Class>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: AMixin<any>.(Anonymous class);
} & T
AMixin
,
const BMixin: <T extends Class<{
x: number;
}>>(Base: T) => {
new (...args: any[]): (Anonymous class);
prototype: BMixin<any>.(Anonymous class);
} & T
BMixin
) {}

Next, we can add some utility functions to make working with these categories just as nice as working with _tags.

First, we add a type guard:

const
const hasCategory: <Category extends symbol>(sym: Category) => <A>(x: A) => x is Extract<A, Record<Category, any>>
hasCategory
=
<
function (type parameter) Category in <Category extends symbol>(sym: Category): <A>(x: A) => x is Extract<A, Record<Category, any>>
Category
extends symbol>(
sym: Category extends symbol
sym
:
function (type parameter) Category in <Category extends symbol>(sym: Category): <A>(x: A) => x is Extract<A, Record<Category, any>>
Category
) =>
<
function (type parameter) A in <A>(x: A): x is Extract<A, Record<Category, any>>
A
,>(
x: A
x
:
function (type parameter) A in <A>(x: A): x is Extract<A, Record<Category, any>>
A
):
x: A
x
is
type Extract<T, U> = T extends U ? T : never

Extract from T those types that are assignable to U

Extract
<
function (type parameter) A in <A>(x: A): x is Extract<A, Record<Category, any>>
A
,
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<
function (type parameter) Category in <Category extends symbol>(sym: Category): <A>(x: A) => x is Extract<A, Record<Category, any>>
Category
, any>> => {
return
import Predicate
Predicate
.
const hasProperty: <Category>(self: unknown, property: Category) => self is { [K in Category]: unknown; } (+1 overload)

Checks whether a value is an object containing a specified property key.

@since2.0.0

hasProperty
(
x: A
x
,
sym: Category extends symbol
sym
);
};

Next, using that guard we can write a catchCategory function which works just like catchTag but on categories instead of tags. Just like catchTag it properly narrows the output type to not include caught errors:

const
const catchCategory: <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>) => <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
catchCategory
=
<
function (type parameter) E in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E
,
function (type parameter) Category in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
Category
extends symbol,
function (type parameter) B in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
B
,
function (type parameter) E2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E2
,
function (type parameter) R2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R2
>(
category: Category extends symbol
category
:
function (type parameter) Category in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
Category
,
f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>
f
: (
error: Extract<E, Record<Category, any>>
error
:
type Extract<T, U> = T extends U ? T : never

Extract from T those types that are assignable to U

Extract
<
function (type parameter) E in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E
,
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<
function (type parameter) Category in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
Category
, any>>) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<
function (type parameter) B in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
B
,
function (type parameter) E2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E2
,
function (type parameter) R2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R2
>,
) =>
<
function (type parameter) A in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
A
,
function (type parameter) R in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R
>(
effect: Effect.Effect<A, E, R>
effect
:
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<
function (type parameter) A in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
A
,
function (type parameter) E in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E
,
function (type parameter) R in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R
>,
):
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<
function (type parameter) A in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
A
|
function (type parameter) B in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
B
,
type Exclude<T, U> = T extends U ? never : T

Exclude from T those types that are assignable to U

Exclude
<
function (type parameter) E in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E
,
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<
function (type parameter) Category in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
Category
, any>> |
function (type parameter) E2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
E2
,
function (type parameter) R in <A, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R
|
function (type parameter) R2 in <E, Category extends symbol, B, E2, R2>(category: Category, f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>): <A, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A | B, Exclude<E, Record<Category, any>> | E2, R | R2>
R2
> =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchIf: <A, E, R, Extract<E, Record<Category, any>>, B, E2, R2>(self: Effect.Effect<A, E, R>, refinement: Predicate.Refinement<E, Extract<E, Record<Category, any>>>, f: (e: Extract<...>) => Effect.Effect<...>) => Effect.Effect<...> (+3 overloads)

Recovers from specific errors based on a predicate.

When to Use

catchIf works similarly to

catchSome

, but it allows you to recover from errors by providing a predicate function. If the predicate matches the error, the recovery effect is applied. This function doesn't alter the error type, so the resulting effect still carries the original error type unless a user-defined type guard is used to narrow the type.

Example (Catching Specific Errors with a Predicate)

import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
Effect.catchIf(
// Only handle HttpError errors
(error) => error._tag === "HttpError",
() => Effect.succeed("Recovering from HttpError")
)
)

@since2.0.0

catchIf
(
effect: Effect.Effect<A, E, R>
effect
,
const hasCategory: <Category>(sym: Category) => <A>(x: A) => x is Extract<A, Record<Category, any>>
hasCategory
(
category: Category extends symbol
category
),
f: (error: Extract<E, Record<Category, any>>) => Effect.Effect<B, E2, R2>
f
) as any;
declare const
const example: Effect.Effect<void, FooError | BarError | BazError, never>
example
:
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<void,
class FooError
FooError
|
class BarError
BarError
|
class BazError
BazError
>;
// only FooError left (A's removed)
const test1 =
const example: Effect.Effect<void, FooError | BarError | BazError, never>
example
.
Pipeable.pipe<Effect.Effect<void, FooError | BarError | BazError, never>, Effect.Effect<void, FooError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, FooError | ... 1 more ... | BazError, never>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
const catchCategory: <FooError | BarError | BazError, typeof CategoryA, void, never, never>(category: typeof CategoryA, f: (error: BarError | BazError) => Effect.Effect<...>) => <A, R>(effect: Effect.Effect<...>) => Effect.Effect<...>
catchCategory
(
const CategoryA: typeof CategoryA
CategoryA
, (
error: BarError | BazError
error
) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const void: Effect.Effect<void, never, never>
export void

Represents an effect that does nothing and produces no value.

When to Use

Use this effect when you need to represent an effect that does nothing. This is useful in scenarios where you need to satisfy an effect-based interface or control program flow without performing any operations. For example, it can be used in situations where you want to return an effect from a function but do not need to compute or return any result.

@since2.0.0

void
));
const test1: Effect.Effect<void, FooError, never>
// example of using added behavior
const
const test2: Effect.Effect<void, FooError | BarError, never>
test2
=
const example: Effect.Effect<void, FooError | BarError | BazError, never>
example
.
Pipeable.pipe<Effect.Effect<void, FooError | BarError | BazError, never>, Effect.Effect<void, FooError | BarError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
const catchCategory: <FooError | BarError | BazError, typeof CategoryB, void, never, never>(category: typeof CategoryB, f: (error: BazError) => Effect.Effect<...>) => <A, R>(effect: Effect.Effect<...>) => Effect.Effect<...>
catchCategory
(
const CategoryB: typeof CategoryB
CategoryB
, (
error: BazError
error
) =>
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level.

Details

This function provides a simple way to log messages or error causes during the execution of your effects. By default, logs are recorded at the INFO level, but this can be adjusted using other logging utilities (Logger.withMinimumLogLevel). Multiple items, including Cause instances, can be logged in a single call. When logging Cause instances, detailed error information is included in the log output.

The log output includes useful metadata like the current timestamp, log level, and fiber ID, making it suitable for debugging and tracking purposes. This function does not interrupt or alter the effect's execution flow.

Example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
(
error: BazError
error
.
function (Anonymous class)<{ new (...args: any[]): AMixin<TaggedErrorClass<BazError, "BazError", { readonly _tag: tag<"BazError">; } & { x: typeof Number$; }>>.(Anonymous class); prototype: AMixin<...>.(Anonymous class); } & TaggedErrorClass<...>>.double(): number
double
())),
);

Conclusion

This pattern is pretty neat, decently practical and doesn’t impact any of the existing _tag ways of interacting with errors.

Check it out and let me know what you think. Here is a link to a full example in the Effect playground.

Thank you to Tim Smart for his feedback on this pattern and for authoring the catchCategory function.

UPDATE: Michael Arnaldi shared his own version of this pattern that works with string literals and has some other niceties like being able to catch multiple categories at once