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"constdivide = (a : number,b : number):Effect .Effect <number> =>b === 0?Effect .die (newError ("Cannot divide by zero")):Effect .succeed (a /b )Effect .runSync (divide (1, 0)) // throws Error: Cannot divide by zero
ts
import {Effect } from "effect"constdivide = (a : number,b : number):Effect .Effect <number> =>b === 0?Effect .die (newError ("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"constdivide = (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"constdivide = (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"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constprogram =Effect .orDie (divide (1, 0))Effect .runSync (program ) // throws Error: Cannot divide by zero
ts
import {Effect } from "effect"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constprogram =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"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constprogram =Effect .orDieWith (divide (1, 0),(error ) => newError (`defect: ${error .message }`))Effect .runSync (program ) // throws Error: defect: Cannot divide by zero
ts
import {Effect } from "effect"constdivide = (a : number,b : number):Effect .Effect <number,Error > =>b === 0?Effect .fail (newError ("Cannot divide by zero")):Effect .succeed (a /b )constprogram =Effect .orDieWith (divide (1, 0),(error ) => newError (`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.
exit
The Effect.exit
function transforms an Effect<A, E, R>
into an effect that encapsulates both potential failure and success within an Exit data type:
ts
Effect<A, E, R> -> Effect<Exit<A, E>, never, R>
ts
Effect<A, E, R> -> Effect<Exit<A, E>, never, R>
The resulting effect cannot fail because the potential failure is now represented within the Exit
's Failure
type.
The error type of the returned Effect
is specified as never
, confirming that the effect is structured to not fail.
By yielding an Exit
, we gain the ability to "pattern match" on this type to handle both failure and success cases within the generator function.
ts
import {Effect ,Cause ,Console ,Exit } from "effect"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .gen (function* () {constexit = yield*Effect .exit (task )if (Exit .isFailure (exit )) {constcause =exit .cause if (Cause .isDieType (cause ) &&Cause .isRuntimeException (cause .defect )) {yield*Console .log (`RuntimeException defect caught: ${cause .defect .message }`)} else {yield*Console .log ("Unknown defect caught.")}}})// We get an Exit.Success because we caught all defectsEffect .runPromiseExit (program ).then (console .log )/*Output:RuntimeException defect caught: Boom!{_id: "Exit",_tag: "Success",value: undefined}*/
ts
import {Effect ,Cause ,Console ,Exit } from "effect"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .gen (function* () {constexit = yield*Effect .exit (task )if (Exit .isFailure (exit )) {constcause =exit .cause if (Cause .isDieType (cause ) &&Cause .isRuntimeException (cause .defect )) {yield*Console .log (`RuntimeException defect caught: ${cause .defect .message }`)} else {yield*Console .log ("Unknown defect caught.")}}})// We get an Exit.Success because we caught all defectsEffect .runPromiseExit (program ).then (console .log )/*Output:RuntimeException defect caught: Boom!{_id: "Exit",_tag: "Success",value: undefined}*/
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"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .catchAllDefect (task , (defect ) => {if (Cause .isRuntimeException (defect )) {returnConsole .log (`RuntimeException defect caught: ${defect .message }`)}returnConsole .log ("Unknown defect caught.")})// We get an Exit.Success because we caught all defectsEffect .runPromiseExit (program ).then (console .log )/*Output:RuntimeException defect caught: Boom!{_id: "Exit",_tag: "Success",value: undefined}*/
ts
import {Effect ,Cause ,Console } from "effect"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .catchAllDefect (task , (defect ) => {if (Cause .isRuntimeException (defect )) {returnConsole .log (`RuntimeException defect caught: ${defect .message }`)}returnConsole .log ("Unknown defect caught.")})// We get an Exit.Success because we caught all defectsEffect .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"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .catchSomeDefect (task , (defect ) => {if (Cause .isIllegalArgumentException (defect )) {returnOption .some (Console .log (`Caught an IllegalArgumentException defect: ${defect .message }`))}returnOption .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' } }}*/
ts
import {Effect ,Cause ,Option ,Console } from "effect"// Simulating a runtime errorconsttask =Effect .dieMessage ("Boom!")constprogram =Effect .catchSomeDefect (task , (defect ) => {if (Cause .isIllegalArgumentException (defect )) {returnOption .some (Console .log (`Caught an IllegalArgumentException defect: ${defect .message }`))}returnOption .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' } }}*/
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
).