Error Channel Operations

On this page

In Effect you can perform various operations on the error channel of effects. These operations allow you to transform, inspect, and handle errors in different ways. Let's explore some of these operations.

Map Operations

mapError

The Effect.mapError function is used when you need to transform or modify an error produced by an effect, without affecting the success value. This can be helpful when you want to add extra information to the error or change its type.

ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
 
const mapped = Effect.mapError(simulatedTask, (message) => new Error(message))
ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
 
const mapped = Effect.mapError(simulatedTask, (message) => new Error(message))

We can observe that the type in the error channel of our program has changed from string to Error.

It's important to note that using the Effect.mapError function does not change the overall success or failure of the effect. If the mapped effect is successful, then the mapping function is ignored. In other words, the Effect.mapError operation only applies the transformation to the error channel of the effect, while leaving the success channel unchanged.

mapBoth

The Effect.mapBoth function allows you to apply transformations to both channels: the error channel and the success channel of an effect. It takes two map functions as arguments: one for the error channel and the other for the success channel.

ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
 
const modified = Effect.mapBoth(simulatedTask, {
onFailure: (message) => new Error(message),
onSuccess: (n) => n > 0
})
ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh no!").pipe(Effect.as(1))
 
const modified = Effect.mapBoth(simulatedTask, {
onFailure: (message) => new Error(message),
onSuccess: (n) => n > 0
})

After using mapBoth, we can observe that the type of our program has changed from Effect<number, string> to Effect<boolean, Error>.

It's important to note that using the mapBoth function does not change the overall success or failure of the effect. It only transforms the values in the error and success channels while preserving the effect's original success or failure status.

Filtering the Success Channel

The Effect library provides several operators to filter values on the success channel based on a given predicate. These operators offer different strategies for handling cases where the predicate fails. Let's explore them:

FunctionDescription
Effect.filterOrFailThis operator filters the values on the success channel based on a predicate. If the predicate fails for any value, the original effect fails with an error.
Effect.filterOrDie and Effect.filterOrDieMessageThese operators also filter the values on the success channel based on a predicate. If the predicate fails for any value, the original effect terminates abruptly. The filterOrDieMessage variant allows you to provide a custom error message.
Effect.filterOrElseThis operator filters the values on the success channel based on a predicate. If the predicate fails for any value, an alternative effect is executed instead.

Here's an example that demonstrates these filtering operators in action:

ts
import { Effect, Random, Cause } from "effect"
 
const task1 = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "random number is negative"
)
 
const task2 = Effect.filterOrDie(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => new Cause.IllegalArgumentException("random number is negative")
)
 
const task3 = Effect.filterOrDieMessage(
Random.nextRange(-1, 1),
(n) => n >= 0,
"random number is negative"
)
 
const task4 = Effect.filterOrElse(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => task3
)
ts
import { Effect, Random, Cause } from "effect"
 
const task1 = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "random number is negative"
)
 
const task2 = Effect.filterOrDie(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => new Cause.IllegalArgumentException("random number is negative")
)
 
const task3 = Effect.filterOrDieMessage(
Random.nextRange(-1, 1),
(n) => n >= 0,
"random number is negative"
)
 
const task4 = Effect.filterOrElse(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => task3
)

It's important to note that depending on the specific filtering operator used, the effect can either fail, terminate abruptly, or execute an alternative effect when the predicate fails. Choose the appropriate operator based on your desired error handling strategy and program logic.

In addition to the filtering capabilities discussed earlier, you have the option to further refine and narrow down the type of the success channel by providing a user-defined type guard to the filterOr* APIs. This not only enhances type safety but also improves code clarity. Let's explore this concept through an example:

ts
import { Effect, pipe } from "effect"
 
// Define a user interface
interface User {
readonly name: string
}
 
// Assume an asynchronous authentication function
declare const auth: () => Promise<User | null>
 
const program = pipe(
Effect.promise(() => auth()),
Effect.filterOrFail(
// Define a guard to narrow down the type
(user): user is User => user !== null,
() => new Error("Unauthorized")
),
Effect.map((user) => user.name) // The 'user' here has type `User`, not `User | null`
)
ts
import { Effect, pipe } from "effect"
 
// Define a user interface
interface User {
readonly name: string
}
 
// Assume an asynchronous authentication function
declare const auth: () => Promise<User | null>
 
const program = pipe(
Effect.promise(() => auth()),
Effect.filterOrFail(
// Define a guard to narrow down the type
(user): user is User => user !== null,
() => new Error("Unauthorized")
),
Effect.map((user) => user.name) // The 'user' here has type `User`, not `User | null`
)

In the example above, a guard is used within the filterOrFail API to ensure that the user is of type User rather than User | null. This refined type information improves the reliability of your code and makes it more understandable.

If you prefer, you can utilize a pre-made guard like Predicate.isNotNull for simplicity and consistency.

Inspecting Errors

Similar to tapping for success values, Effect provides several operators for inspecting error values. These operators allow us to peek into failures or underlying defects or causes:

  • tapError
  • tapBoth
  • tapErrorCause
  • tapDefect

Let's see an example of how to use these operators:

ts
import { Effect, Random, Console } from "effect"
 
const task = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "random number is negative"
)
 
const tapping1 = Effect.tapError(task, (error) =>
Console.log(`failure: ${error}`)
)
 
const tapping2 = Effect.tapBoth(task, {
onFailure: (error) => Console.log(`failure: ${error}`),
onSuccess: (randomNumber) => Console.log(`random number: ${randomNumber}`)
})
ts
import { Effect, Random, Console } from "effect"
 
const task = Effect.filterOrFail(
Random.nextRange(-1, 1),
(n) => n >= 0,
() => "random number is negative"
)
 
const tapping1 = Effect.tapError(task, (error) =>
Console.log(`failure: ${error}`)
)
 
const tapping2 = Effect.tapBoth(task, {
onFailure: (error) => Console.log(`failure: ${error}`),
onSuccess: (randomNumber) => Console.log(`random number: ${randomNumber}`)
})

It's important to note that tapping into error values does not change the type of the program.

Exposing Errors in The Success Channel

You can use the Effect.either function to convert 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 resulting effect is an unexceptional effect, which means it cannot fail, because the failure case has been exposed as part of the Either left case. Therefore, the error parameter of the returned Effect is never, as it is guaranteed that the effect does not model failure.

This function becomes especially useful when recovering from effects that may fail when using Effect.gen.


ts
import { Effect, Either, Console } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(simulatedTask))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
yield* _(Console.log(`failure: ${error}`))
return 0
} else {
const value = failureOrSuccess.right
yield* _(Console.log(`success: ${value}`))
return value
}
})
ts
import { Effect, Either, Console } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(simulatedTask))
if (Either.isLeft(failureOrSuccess)) {
const error = failureOrSuccess.left
yield* _(Console.log(`failure: ${error}`))
return 0
} else {
const value = failureOrSuccess.right
yield* _(Console.log(`success: ${value}`))
return value
}
})

Exposing the Cause in The Success Channel

You can use the Effect.cause function to expose the cause of an effect, which is a more detailed representation of failures, including error messages and defects.


ts
import { Effect, Console } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const cause = yield* _(Effect.cause(simulatedTask))
yield* _(Console.log(cause))
})
ts
import { Effect, Console } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const cause = yield* _(Effect.cause(simulatedTask))
yield* _(Console.log(cause))
})

Merging the Error Channel into the Success Channel

Using the Effect.merge function, you can merge the error channel into the success channel, creating an effect that always succeeds with the merged value.

ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const merged = Effect.merge(simulatedTask)
ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const merged = Effect.merge(simulatedTask)

Flipping Error and Success Channels

Using the Effect.flip function, you can flip the error and success channels of an effect, effectively swapping their roles.

ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const flipped = Effect.flip(simulatedTask)
ts
import { Effect } from "effect"
 
const simulatedTask = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const flipped = Effect.flip(simulatedTask)