Unexpected Errors

On this page

There are situations where you may encounter unexpected errors, and you need to decide how to handle them. Effect provides functions to help you deal with such scenarios, allowing you to take appropriate actions when errors occur during the execution of your effects.

Creating Unrecoverable Errors

In the same way it is possible to leverage combinators such as fail to create values of type Effect<never, E, never> the Effect library provides tools to create defects.

Creating defects is a common necessity when dealing with errors from which it is not possible to recover from a business logic perspective, such as attempting to establish a connection that is refused after multiple retries.

In those cases terminating the execution of the effect and moving into reporting, through an output such as stdout or some external monitoring service, might be the best solution.

The following functions and combinators allow for termination of the effect and are often used to convert values of type Effect<A, E, R> into values of type Effect<A, never, R> allowing the programmer an escape hatch from having to handle and recover from errors for which there is no sensible way to recover.

die / dieMessage

The Effect.die function returns an effect that throws a specified error, while Effect.dieMessage throws a RuntimeException with a specified text message. These functions are useful for terminating a fiber when a defect, a critical and unexpected error, is detected in the code.

Example using die:

ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number> =>
b === 0
? Effect.die(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
Effect.runSync(divide(1, 0)) // throws Error: Cannot divide by zero
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number> =>
b === 0
? Effect.die(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
Effect.runSync(divide(1, 0)) // throws Error: Cannot divide by zero

Example using dieMessage:

ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number> =>
b === 0 ? Effect.dieMessage("Cannot divide by zero") : Effect.succeed(a / b)
 
Effect.runSync(divide(1, 0)) // throws RuntimeException: Cannot divide by zero
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number> =>
b === 0 ? Effect.dieMessage("Cannot divide by zero") : Effect.succeed(a / b)
 
Effect.runSync(divide(1, 0)) // throws RuntimeException: Cannot divide by zero

orDie

The Effect.orDie function transforms an effect's failure into a termination of the fiber, making all failures unchecked and not part of the type of the effect. It can be used to handle errors that you do not wish to recover from.

ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.orDie(divide(1, 0))
 
Effect.runSync(program) // throws Error: Cannot divide by zero
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.orDie(divide(1, 0))
 
Effect.runSync(program) // throws Error: Cannot divide by zero

After using Effect.orDie, the error channel type of the program is never, indicating that all failures are unchecked, and the effect is expected to terminate the fiber when an error occurs.

orDieWith

Similar to Effect.orDie, the Effect.orDieWith function transforms an effect's failure into a termination of the fiber using a specified mapping function. It allows you to customize the error message before terminating the fiber.

ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.orDieWith(
divide(1, 0),
(error) => new Error(`defect: ${error.message}`)
)
 
Effect.runSync(program) // throws Error: defect: Cannot divide by zero
ts
import { Effect } from "effect"
 
const divide = (a: number, b: number): Effect.Effect<number, Error> =>
b === 0
? Effect.fail(new Error("Cannot divide by zero"))
: Effect.succeed(a / b)
 
const program = Effect.orDieWith(
divide(1, 0),
(error) => new Error(`defect: ${error.message}`)
)
 
Effect.runSync(program) // throws Error: defect: Cannot divide by zero

After using Effect.orDieWith, the error channel type of the program is never, just like with Effect.orDie.

Catching

Effect provides two functions that allow you to handle unexpected errors that may occur during the execution of your effects.

There is no sensible way to recover from defects. The functions we're about to discuss should be used only at the boundary between Effect and an external system, to transmit information on a defect for diagnostic or explanatory purposes.

catchAllDefect

The Effect.catchAllDefect function allows you to recover from all defects using a provided function. Here's an example:

ts
import { Effect, Cause, Console } from "effect"
 
const program = Effect.catchAllDefect(
Effect.dieMessage("Boom!"), // Simulating a runtime error
(defect) => {
if (Cause.isRuntimeException(defect)) {
return Console.log(`RuntimeException defect caught: ${defect.message}`)
}
return Console.log("Unknown defect caught.")
}
)
 
// We get an Exit.Success because we caught all defects
Effect.runPromiseExit(program).then(console.log)
/*
Output:
RuntimeException defect caught: Boom!
{
_id: "Exit",
_tag: "Success",
value: undefined
}
*/
ts
import { Effect, Cause, Console } from "effect"
 
const program = Effect.catchAllDefect(
Effect.dieMessage("Boom!"), // Simulating a runtime error
(defect) => {
if (Cause.isRuntimeException(defect)) {
return Console.log(`RuntimeException defect caught: ${defect.message}`)
}
return Console.log("Unknown defect caught.")
}
)
 
// We get an Exit.Success because we caught all defects
Effect.runPromiseExit(program).then(console.log)
/*
Output:
RuntimeException defect caught: Boom!
{
_id: "Exit",
_tag: "Success",
value: undefined
}
*/

It's important to understand that catchAllDefect can only handle defects, not expected errors (such as those caused by Effect.fail) or interruptions in execution (such as when using Effect.interrupt).

A defect refers to an error that cannot be anticipated in advance, and there is no reliable way to respond to it. As a general rule, it's recommended to let defects crash the application, as they often indicate serious issues that need to be addressed.

However, in some specific cases, such as when dealing with dynamically loaded plugins, a controlled recovery approach might be necessary. For example, if our application supports runtime loading of plugins and a defect occurs within a plugin, we may choose to log the defect and then reload only the affected plugin instead of crashing the entire application. This allows for a more resilient and uninterrupted operation of the application.

catchSomeDefect

The Effect.catchSomeDefect function in Effect allows you to recover from specific defects using a provided partial function. Let's take a look at an example:

ts
import { Effect, Cause, Option, Console } from "effect"
 
const program = Effect.catchSomeDefect(
Effect.dieMessage("Boom!"), // Simulating a runtime error
(defect) => {
if (Cause.isIllegalArgumentException(defect)) {
return Option.some(
Console.log(
`Caught an IllegalArgumentException defect: ${defect.message}`
)
)
}
return Option.none()
}
)
 
// Since we are only catching IllegalArgumentException
// we will get an Exit.Failure because we simulated a runtime error.
Effect.runPromiseExit(program).then(console.log)
/*
Output:
{
_id: "Exit",
_tag: "Failure",
cause: {
_id: "Cause",
_tag: "Die",
defect: {
_tag: "RuntimeException",
message: "Boom!",
[Symbol(@effect/io/Cause/errors/RuntimeException)]: Symbol(@effect/io/Cause/errors/RuntimeException),
toString: [Function: toString]
}
}
}
*/
ts
import { Effect, Cause, Option, Console } from "effect"
 
const program = Effect.catchSomeDefect(
Effect.dieMessage("Boom!"), // Simulating a runtime error
(defect) => {
if (Cause.isIllegalArgumentException(defect)) {
return Option.some(
Console.log(
`Caught an IllegalArgumentException defect: ${defect.message}`
)
)
}
return Option.none()
}
)
 
// Since we are only catching IllegalArgumentException
// we will get an Exit.Failure because we simulated a runtime error.
Effect.runPromiseExit(program).then(console.log)
/*
Output:
{
_id: "Exit",
_tag: "Failure",
cause: {
_id: "Cause",
_tag: "Die",
defect: {
_tag: "RuntimeException",
message: "Boom!",
[Symbol(@effect/io/Cause/errors/RuntimeException)]: Symbol(@effect/io/Cause/errors/RuntimeException),
toString: [Function: toString]
}
}
}
*/

It's important to understand that catchSomeDefect can only handle defects, not expected errors (such as those caused by Effect.fail) or interruptions in execution (such as when using Effect.interrupt).