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>
ts
Effect<never, HttpError>

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"
 
class FooError {
readonly _tag = "FooError"
}
 
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"
 
class FooError {
readonly _tag = "FooError"
}
 
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>
ts
Effect<string, FooError | BarError>

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.runPromise(program).then(console.log, console.error)
/*
Output:
Executing task1...
{
_id: "FiberFailure",
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.runPromise(program).then(console.log, console.error)
/*
Output:
Executing task1...
{
_id: "FiberFailure",
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


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<E, A> data type:

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

The Either<E, A> data type represents a value that can be either an error (E) or a successful result (A). 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
})
})

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

ts
Effect<string>
ts
Effect<string>

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.

ts
Effect<string, BarError>
ts
Effect<string, BarError>

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.