Expected Errors

On this page

In this guide you will learn:

  • How Effect represents expected errors
  • The tools Effect provides for robust and comprehensive error management

As we saw in the guide Creating Effects, we can use the fail constructor to create an Effect that represents an error:

ts
import { Effect } from "effect"
 
class HttpError {
readonly _tag = "HttpError"
}
 
const program = Effect.fail(new HttpError())
ts
import { Effect } from "effect"
 
class HttpError {
readonly _tag = "HttpError"
}
 
const program = Effect.fail(new HttpError())

We use a class to represent the HttpError type above simply to gain access to both the error type and a free constructor. However, you can use whatever you like to model your error types.

It's worth noting that we added a readonly _tag field as discriminant to our error in the example:

ts
class HttpError {
readonly _tag = "HttpError"
}
ts
class HttpError {
readonly _tag = "HttpError"
}

Adding a discriminant field, such as _tag, can be beneficial for distinguishing between different types of errors during error handling. It also prevents TypeScript from unifying types, ensuring that each error is treated uniquely based on its discriminant value.

Expected errors are tracked at the type level by the Effect data type in the "Error" channel.

It is evident from the type of program that can fail with an error of type HttpError:

ts
Effect<never, HttpError, never>
ts
Effect<never, HttpError, never>

Error Tracking

The following program serves as an illustration of how errors are automatically tracked for you:


error-tracking.ts
ts
import { Effect, Random } from "effect"
 
export class FooError {
readonly _tag = "FooError"
}
 
export class BarError {
readonly _tag = "BarError"
}
 
export const program = Effect.gen(function* (_) {
const n1 = yield* _(Random.next)
const n2 = yield* _(Random.next)
 
const foo = n1 > 0.5 ? "yay!" : yield* _(Effect.fail(new FooError()))
 
const bar = n2 > 0.5 ? "yay!" : yield* _(Effect.fail(new BarError()))
 
return foo + bar
})
 
Effect.runPromise(program).then(console.log, console.error)
error-tracking.ts
ts
import { Effect, Random } from "effect"
 
export class FooError {
readonly _tag = "FooError"
}
 
export class BarError {
readonly _tag = "BarError"
}
 
export const program = Effect.gen(function* (_) {
const n1 = yield* _(Random.next)
const n2 = yield* _(Random.next)
 
const foo = n1 > 0.5 ? "yay!" : yield* _(Effect.fail(new FooError()))
 
const bar = n2 > 0.5 ? "yay!" : yield* _(Effect.fail(new BarError()))
 
return foo + bar
})
 
Effect.runPromise(program).then(console.log, console.error)

In the above program, we compute two values: foo and bar, each representing a potential source of error.

Effect automatically keeps track of the possible errors that can occur during the execution of the program. In this case, we have FooError and BarError as the possible error types. The error channel of the program is specified as

ts
Effect<string, FooError | BarError, never>
ts
Effect<string, FooError | BarError, never>

indicating that it can potentially fail with either a FooError or a BarError.

Short-Circuiting

When working with APIs like Effect.gen, Effect.map, Effect.flatMap, and Effect.all, it's important to understand how they handle errors. These APIs are designed to short-circuit the execution upon encountering the first error.

What does this mean for you as a developer? Well, let's say you have a chain of operations or a collection of effects to be executed in sequence. If any error occurs during the execution of one of these effects, the remaining computations will be skipped, and the error will be propagated to the final result.

In simpler terms, the short-circuiting behavior ensures that if something goes wrong at any step of your program, it won't waste time executing unnecessary computations. Instead, it will immediately stop and return the error to let you know that something went wrong.


ts
import { Effect, Console } from "effect"
 
// Define three effects representing different tasks.
const task1 = Console.log("Executing task1...")
const task2 = Effect.fail("Something went wrong!")
const task3 = Console.log("Executing task3...")
 
// Compose the three tasks to run them in sequence.
// If one of the tasks fails, the subsequent tasks won't be executed.
const program = Effect.gen(function* (_) {
yield* _(task1)
yield* _(task2) // After task1, task2 is executed, but it fails with an error
yield* _(task3) // This computation won't be executed because the previous one fails
})
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Executing task1...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }
}
*/
ts
import { Effect, Console } from "effect"
 
// Define three effects representing different tasks.
const task1 = Console.log("Executing task1...")
const task2 = Effect.fail("Something went wrong!")
const task3 = Console.log("Executing task3...")
 
// Compose the three tasks to run them in sequence.
// If one of the tasks fails, the subsequent tasks won't be executed.
const program = Effect.gen(function* (_) {
yield* _(task1)
yield* _(task2) // After task1, task2 is executed, but it fails with an error
yield* _(task3) // This computation won't be executed because the previous one fails
})
 
Effect.runPromiseExit(program).then(console.log)
/*
Output:
Executing task1...
{
_id: 'Exit',
_tag: 'Failure',
cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong!' }
}
*/

This code snippet demonstrates the short-circuiting behavior when an error occurs. Each operation depends on the successful execution of the previous one. If any error occurs, the execution is short-circuited, and the error is propagated. In this specific example, task3 is never executed because an error occurs in task2.

Catching all Errors

either

The Effect.either function converts an Effect<A, E, R> into another effect where both its failure (E) and success (A) channels have been lifted into an Either<A, E> data type:

ts
Effect<A, E, R> -> Effect<Either<A, E>, never, R>
ts
Effect<A, E, R> -> Effect<Either<A, E>, never, R>

The Either<R, L> data type represents a value that can be either a Right value (R) or a Left value (L). By yielding an Either, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function.

ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
// failure case: you can extract the error from the `left` property
const error = failureOrSuccess.left
return `Recovering from ${error._tag}`
} else {
// success case: you can extract the value from the `right` property
return failureOrSuccess.right
}
})
ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
// failure case: you can extract the error from the `left` property
const error = failureOrSuccess.left
return `Recovering from ${error._tag}`
} else {
// success case: you can extract the value from the `right` property
return failureOrSuccess.right
}
})

We can make the code less verbose by using the Either.match function, which directly accepts the two callback functions for handling errors and successful values:

ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
return Either.match(failureOrSuccess, {
onLeft: (error) => `Recovering from ${error._tag}`,
onRight: (value) => value // do nothing in case of success
})
})
ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
return Either.match(failureOrSuccess, {
onLeft: (error) => `Recovering from ${error._tag}`,
onRight: (value) => value // do nothing in case of success
})
})

catchAll

The Effect.catchAll function allows you to catch any error that occurs in the program and provide a fallback.

ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchAll((error) => Effect.succeed(`Recovering from ${error._tag}`))
)
ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchAll((error) => Effect.succeed(`Recovering from ${error._tag}`))
)

We can observe that the type in the error channel of our program has changed to never, indicating that all errors have been handled.

Catching Some Errors

Suppose we want to handle a specific error, such as FooError.

ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
if (error._tag === "FooError") {
return "Recovering from FooError"
}
return yield* _(Effect.fail(error))
} else {
return failureOrSuccess.right
}
})
ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
if (error._tag === "FooError") {
return "Recovering from FooError"
}
return yield* _(Effect.fail(error))
} else {
return failureOrSuccess.right
}
})

We can observe that the type in the error channel of our program has changed to only show BarError, indicating that FooError has been handled.

If we also want to handle BarError, we can easily add another case to our code:

ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
if (error._tag === "FooError") {
return "Recovering from FooError"
} else {
return "Recovering from BarError"
}
} else {
return failureOrSuccess.right
}
})
ts
import { Effect, Either } from "effect"
import { program } from "./error-tracking"
 
const recovered = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(program))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
if (error._tag === "FooError") {
return "Recovering from FooError"
} else {
return "Recovering from BarError"
}
} else {
return failureOrSuccess.right
}
})

We can observe that the type in the error channel of our program has changed to never, indicating that all errors have been handled.

catchSome

If we want to catch and recover from only some types of errors and effectfully attempt recovery, we can use the Effect.catchSome function:

ts
import { Effect, Option } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchSome((error) => {
if (error._tag === "FooError") {
return Option.some(Effect.succeed("Recovering from FooError"))
}
return Option.none()
})
)
ts
import { Effect, Option } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchSome((error) => {
if (error._tag === "FooError") {
return Option.some(Effect.succeed("Recovering from FooError"))
}
return Option.none()
})
)

In the code above, Effect.catchSome takes a function that examines the error (error) and decides whether to attempt recovery or not. If the error matches a specific condition, recovery can be attempted by returning Option.some(effect). If no recovery is possible, you can simply return Option.none().

It's important to note that while Effect.catchSome lets you catch specific errors, it doesn't alter the error type itself. Therefore, the resulting effect (recovered in this case) will still have the same error type (FooError | BarError) as the original effect.

catchIf

Similar to Effect.catchSome, the function Effect.catchIf allows you to recover from specific errors based on a predicate:

ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchIf(
(error) => error._tag === "FooError",
() => Effect.succeed("Recovering from FooError")
)
)
ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchIf(
(error) => error._tag === "FooError",
() => Effect.succeed("Recovering from FooError")
)
)

It's important to note that while Effect.catchIf lets you catch specific errors, it doesn't alter the error type itself. Therefore, the resulting effect (recovered in this case) will still have the same error type (FooError | BarError) as the original effect.

However, if you provide a user-defined type guard instead of a predicate, the resulting error type will be pruned, returning an Effect<string, BarError, never>:

ts
import { Effect } from "effect"
import { program, FooError } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchIf(
(error): error is FooError => error._tag === "FooError",
() => Effect.succeed("Recovering from FooError")
)
)
ts
import { Effect } from "effect"
import { program, FooError } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchIf(
(error): error is FooError => error._tag === "FooError",
() => Effect.succeed("Recovering from FooError")
)
)

catchTag

If your program's errors are all tagged with a _tag field that acts as a discriminator you can use the Effect.catchTag function to catch and handle specific errors with precision.

ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTag("FooError", (_fooError) =>
Effect.succeed("Recovering from FooError")
)
)
ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTag("FooError", (_fooError) =>
Effect.succeed("Recovering from FooError")
)
)

In the example above, the Effect.catchTag function allows us to handle FooError specifically. If a FooError occurs during the execution of the program, the provided error handler function will be invoked, and the program will proceed with the recovery logic specified within the handler.

We can observe that the type in the error channel of our program has changed to only show BarError, indicating that FooError has been handled.

If we also wanted to handle BarError, we can simply add another catchTag:

ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTag("FooError", (_fooError) =>
Effect.succeed("Recovering from FooError")
),
Effect.catchTag("BarError", (_barError) =>
Effect.succeed("Recovering from BarError")
)
)
ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTag("FooError", (_fooError) =>
Effect.succeed("Recovering from FooError")
),
Effect.catchTag("BarError", (_barError) =>
Effect.succeed("Recovering from BarError")
)
)

We can observe that the type in the error channel of our program has changed to never, indicating that all errors have been handled.

It is important to ensure that the error type used with catchTag has a readonly _tag discriminant field. This field is required for the matching and handling of specific error tags.

catchTags

Instead of using the Effect.catchTag function multiple times to handle individual error types, we have a more convenient option called Effect.catchTags. With Effect.catchTags, we can handle multiple errors in a single block of code.

ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTags({
FooError: (_fooError) => Effect.succeed(`Recovering from FooError`),
BarError: (_barError) => Effect.succeed(`Recovering from BarError`)
})
)
ts
import { Effect } from "effect"
import { program } from "./error-tracking"
 
const recovered = program.pipe(
Effect.catchTags({
FooError: (_fooError) => Effect.succeed(`Recovering from FooError`),
BarError: (_barError) => Effect.succeed(`Recovering from BarError`)
})
)

In the above example, instead of using Effect.catchTag multiple times to handle individual errors, we utilize the Effect.catchTags combinator. This combinator takes an object where each property represents a specific error _tag ("FooError" and "BarError" in this case), and the corresponding value is the error handler function to be executed when that particular error occurs.

It is important to ensure that all the error types used with Effect.catchTags have a readonly _tag discriminant field. This field is required for the matching and handling of specific error tags.