Skip to content

Error Handling

Premise

It is required to handle errors in Stanzas. Failure to appropriately catch known errors results in static type analysis failing at Sonnet creation:

declare const
const canFail: Effect.Effect<void, Error, never>
canFail
:
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0

@since2.0.0

Effect
<void,
interface Error
Error
, never>
import Sonnet
Sonnet
.
const make: <never, never>(rootEffect: Effect.Effect<void, never, Sonnet.SonnetService>, layer: Layer<Sonnet.SonnetService, never, never>, memoMap?: MemoMap | undefined) => Sonnet.Sonnet<...>

Construct a redux-sonnet middleware.

NOTE: side-effect handlers are provided up-front to guarantee declarative behavior.

@example

import { configureStore, Tuple as RTKTuple } from "@reduxjs/toolkit"
import { Sonnet, Stanza } from "redux-sonnet"
import { Stream, Layer } from "effect"
const stanza = Stanza.fromStream(Stream.make(
{ type: "ACTION-1" },
{ type: "ACTION-2" },
{ type: "ACTION-3" },
))
const sonnet = Sonnet.make(
// ^?
stanza,
Sonnet.defaultLayer,
)
const store = configureStore({
// ^?
reducer: () => {},
middleware: () => new RTKTuple(sonnet)
})

@since0.0.0

make
(
canFail,
Error ts(2379) ― Argument of type 'Effect<void, Error, never>' is not assignable to parameter of type 'Effect<void, never, SonnetService>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Type 'Error' is not assignable to type 'never'.
import Sonnet
Sonnet
.
const defaultLayer: Layer<Sonnet.SonnetService, never, never>

@since0.0.0

defaultLayer
)

Unhandled Exceptions

If an exception is thrown as part of a Stanza’s execution, the thrown exception will propagate up to the Sonnet’s fiber execution.

Full Example

import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Stream
Stream
,
import Data
Data
,
function flow<A extends ReadonlyArray<unknown>, B = never>(ab: (...a: A) => B): (...a: A) => B (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"
const len = (s: string): number => s.length
const double = (n: number): number => n * 2
const f = flow(len, double)
assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
,
function pipe<A>(a: A): A (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

@example

// Example: Chaining Arithmetic Operations
import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
} from "effect"
import type {
type Action<T extends string = string> = {
type: T;
}

An action is a plain object that represents an intention to change the state. Actions are the only way to get data into the store. Any data, whether from UI events, network callbacks, or other sources such as WebSockets needs to eventually be dispatched as actions.

Actions must have a type field that indicates the type of action being performed. Types can be defined as constants and imported from another module. These must be strings, as strings are serializable.

Other than type, the structure of an action object is really up to you. If you're interested, check out Flux Standard Action for recommendations on how actions should be constructed.

Action
,
type Reducer<S = any, A extends Action = UnknownAction, PreloadedState = S> = (state: S | PreloadedState | undefined, action: A) => S

A reducer is a function that accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of values down to a single value

Reducers are not unique to Redux—they are a fundamental concept in functional programming. Even most non-functional languages, like JavaScript, have a built-in API for reducing. In JavaScript, it's Array.prototype.reduce().

In Redux, the accumulated value is the state object, and the values being accumulated are actions. Reducers calculate a new state given the previous state and an action. They must be pure functions—functions that return the exact same output for given inputs. They should also be free of side-effects. This is what enables exciting features like hot reloading and time travel.

Reducers are the most important concept in Redux.

Do not put API calls into reducers.

Reducer
,
(alias) interface UnknownAction
import UnknownAction

An Action type which accepts any other properties. This is mainly for the use of the Reducer type. This is not part of Action itself to prevent types that extend Action from having an index signature.

UnknownAction
} from "redux"
import {
import Actions
Actions
,
import Operators
Operators
,
import Sonnet
Sonnet
} from "redux-sonnet"
class
class FetchError
FetchError
extends
import Data
Data
.
const TaggedError: <"FetchError">(tag: "FetchError") => new <A>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & {
...;
} & Readonly<...>

@since2.0.0

TaggedError
("FetchError")<{}> {}
const tryFetch =
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const tryPromise: <Response, FetchError>(options: {
readonly try: (signal: AbortSignal) => PromiseLike<Response>;
readonly catch: (error: unknown) => FetchError;
}) => Effect.Effect<...> (+1 overload)

Creates an Effect that represents an asynchronous computation that might fail.

When to Use

In situations where you need to perform asynchronous operations that might fail, such as fetching data from an API, you can use the tryPromise constructor. This constructor is designed to handle operations that could throw exceptions by capturing those exceptions and transforming them into manageable errors.

Error Handling

There are two ways to handle errors with tryPromise:

  1. If you don't provide a catch function, the error is caught and the effect fails with an UnknownException.
  2. If you provide a catch function, the error is caught and the catch function maps it to an error of type E.

Interruptions

An optional AbortSignal can be provided to allow for interruption of the wrapped Promise API.

@seepromise if the effectful computation is asynchronous and does not throw errors.

@example

// Title: Fetching a TODO Item
import { Effect } from "effect"
const getTodo = (id: number) =>
// Will catch any errors and propagate them as UnknownException
Effect.tryPromise(() =>
fetch(`https://jsonplaceholder.typicode.com/todos/${id}`)
)
// ┌─── Effect<Response, UnknownException, never>
// ▼
const program = getTodo(1)

@example

// Title: Custom Error Handling import { Effect } from "effect"

const getTodo = (id: number) => Effect.tryPromise({ try: () => fetch(https://jsonplaceholder.typicode.com/todos/${id}), // remap the error catch: (unknown) => new Error(something went wrong ${unknown}) })

// ┌─── Effect<Response, Error, never> // ▼ const program = getTodo(1)

@since2.0.0

tryPromise
({
const tryFetch: Effect.Effect<Response, FetchError, never>
try: (signal: AbortSignal) => PromiseLike<Response>
try
: () =>
function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>
fetch
("https://some.resource"),
catch: (error: unknown) => FetchError
catch
: () => new
constructor FetchError<{}>(args: void): FetchError
FetchError
()
})
const
const fetchAction: Actions.AsyncActionSet<"fetch", void, unknown, FetchError>
fetchAction
=
import Actions
Actions
.
const make: <"fetch">(prefix: "fetch") => <TriggerPayload, FulfilledPayload, RejectedPayload>() => Actions.AsyncActionSet<"fetch", TriggerPayload, FulfilledPayload, RejectedPayload>

Constructs trigger, fulfillment, and rejection actions based on a prefix.

@example

import * as Actions from "redux-sonnet/Actions"
const {
trigger,
fulfilled,
rejected,
} = Actions.make("increment")<void, void, never>()
assert.strictEqual(trigger.type, "increment/trigger")
assert.strictEqual(fulfilled.type, "increment/fulfilled")
assert.strictEqual(rejected.type, "increment/rejected")

@since0.0.0

make
("fetch")<void, unknown,
class FetchError
FetchError
>()
const
const stanza: Effect.Effect<void, never, Sonnet.SonnetService>
stanza
=
pipe<Effect.Effect<Response, FetchError, never>, Effect.Effect<{
payload: Response;
type: "fetch/fulfilled";
}, FetchError, never>, Effect.Effect<...>, Effect.Effect<...>, Effect.Effect<...>>(a: Effect.Effect<...>, ab: (a: Effect.Effect<...>) => Effect.Effect<...>, bc: (b: Effect.Effect<...>) => Effect.Effect<...>, cd: (c: Effect.Effect<...>) => Effect.Effect<...>, de: (d: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"
pipe(as, Array.map(f), Array.filter(g))

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"
const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│ ... │───►│ funcN │───►│ result │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘ └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

@example

// Example: Chaining Arithmetic Operations
import { pipe } from "effect"
// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10
// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)
console.log(result)
// Output: 2

@since2.0.0

pipe
(
const tryFetch: Effect.Effect<Response, FetchError, never>
tryFetch
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const map: <Response, {
payload: Response;
type: "fetch/fulfilled";
}>(f: (a: Response) => {
payload: Response;
type: "fetch/fulfilled";
}) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Transforms the value inside an effect by applying a function to it.

Syntax

const mappedEffect = pipe(myEffect, Effect.map(transformation))
// or
const mappedEffect = Effect.map(myEffect, transformation)
// or
const mappedEffect = myEffect.pipe(Effect.map(transformation))

Details

map takes a function and applies it to the value contained within an effect, creating a new effect with the transformed value.

It's important to note that effects are immutable, meaning that the original effect is not modified. Instead, a new effect is returned with the updated value.

@seemapError for a version that operates on the error channel.

@seemapBoth for a version that operates on both channels.

@seeflatMap or andThen for a version that can return a new effect.

@example

// Title: Adding a Service Charge
import { pipe, Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const finalAmount = pipe(
fetchTransactionAmount,
Effect.map(addServiceCharge)
)
Effect.runPromise(finalAmount).then(console.log)
// Output: 101

@since2.0.0

map
(
const fetchAction: Actions.AsyncActionSet<"fetch", void, unknown, FetchError>
fetchAction
.
AsyncActionSet<"fetch", void, unknown, FetchError>.fulfilled: ActionCreatorWithNonInferrablePayload<"fetch/fulfilled">
fulfilled
),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const flatMap: <{
payload: Response;
type: "fetch/fulfilled";
}, string, never, Sonnet.SonnetService>(f: (a: {
payload: Response;
type: "fetch/fulfilled";
}) => Effect.Effect<string, never, Sonnet.SonnetService>) => <E, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Chains effects to produce new Effect instances, useful for combining operations that depend on previous results.

Syntax

const flatMappedEffect = pipe(myEffect, Effect.flatMap(transformation))
// or
const flatMappedEffect = Effect.flatMap(myEffect, transformation)
// or
const flatMappedEffect = myEffect.pipe(Effect.flatMap(transformation))

When to Use

Use flatMap when you need to chain multiple effects, ensuring that each step produces a new Effect while flattening any nested effects that may occur.

Details

flatMap lets you sequence effects so that the result of one effect can be used in the next step. It is similar to flatMap used with arrays but works specifically with Effect instances, allowing you to avoid deeply nested effect structures.

Since effects are immutable, flatMap always returns a new effect instead of changing the original one.

@example

import { pipe, Effect } from "effect"
// Function to apply a discount safely to a transaction amount
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, Error> =>
discountRate === 0
? Effect.fail(new Error("Discount rate cannot be zero"))
: Effect.succeed(total - (total * discountRate) / 100)
// Simulated asynchronous task to fetch a transaction amount from database
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
// Chaining the fetch and discount application using `flatMap`
const finalAmount = pipe(
fetchTransactionAmount,
Effect.flatMap((amount) => applyDiscount(amount, 5))
)
Effect.runPromise(finalAmount).then(console.log)
// Output: 95

@since2.0.0

flatMap
(
import Operators
Operators
.
const put: <A extends Action>(self: A) => Effect.Effect<string, never, Sonnet.SonnetService>

Dispatch an Action to the Redux store.

@since0.0.0

put
),
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const asVoid: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, E, R>

This function maps the success value of an Effect value to void. If the original Effect value succeeds, the returned Effect value will also succeed. If the original Effect value fails, the returned Effect value will fail with the same error.

@since2.0.0

asVoid
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const catchTag: <"FetchError", FetchError, void, never, Sonnet.SonnetService>(k: "FetchError", f: (e: FetchError) => Effect.Effect<void, never, Sonnet.SonnetService>) => <A, R>(self: Effect.Effect<...>) => Effect.Effect<...> (+1 overload)

Catches and handles specific errors by their _tag field, which is used as a discriminator.

When to Use

catchTag is useful when your errors are tagged with a readonly _tag field that identifies the error type. You can use this function to handle specific error types by matching the _tag value. This allows for precise error handling, ensuring that only specific errors are caught and handled.

The error type must have a readonly _tag field to use catchTag. This field is used to identify and match errors.

@seecatchTags for a version that allows you to handle multiple error types at once.

@example

// Title: Handling Errors by Tag
import { Effect, Random } from "effect"
class HttpError {
readonly _tag = "HttpError"
}
class ValidationError {
readonly _tag = "ValidationError"
}
// ┌─── Effect<string, HttpError | ValidationError, never>
// ▼
const program = Effect.gen(function* () {
const n1 = yield* Random.next
const n2 = yield* Random.next
if (n1 < 0.5) {
yield* Effect.fail(new HttpError())
}
if (n2 < 0.5) {
yield* Effect.fail(new ValidationError())
}
return "some result"
})
// ┌─── Effect<string, ValidationError, never>
// ▼
const recovered = program.pipe(
// Only handle HttpError errors
Effect.catchTag("HttpError", (_HttpError) =>
Effect.succeed("Recovering from HttpError")
)
)

@since2.0.0

catchTag
(
"FetchError",
flow<[payload: FetchError], {
payload: FetchError;
type: "fetch/rejected";
}, Effect.Effect<string, never, Sonnet.SonnetService>, Effect.Effect<void, never, Sonnet.SonnetService>>(ab: (payload: FetchError) => {
payload: FetchError;
type: "fetch/rejected";
}, bc: (b: {
payload: FetchError;
type: "fetch/rejected";
}) => Effect.Effect<...>, cd: (c: Effect.Effect<...>) => Effect.Effect<...>): (payload: FetchError) => Effect.Effect<...> (+8 overloads)

Performs left-to-right function composition. The first argument may have any arity, the remaining arguments must be unary.

See also pipe.

@example

import { flow } from "effect/Function"
const len = (s: string): number => s.length
const double = (n: number): number => n * 2
const f = flow(len, double)
assert.strictEqual(f('aaa'), 6)

@since2.0.0

flow
(
const fetchAction: Actions.AsyncActionSet<"fetch", void, unknown, FetchError>
fetchAction
.
AsyncActionSet<"fetch", void, unknown, FetchError>.rejected: ActionCreatorWithPayload<FetchError, "fetch/rejected">
rejected
,
import Operators
Operators
.
const put: <A extends Action>(self: A) => Effect.Effect<string, never, Sonnet.SonnetService>

Dispatch an Action to the Redux store.

@since0.0.0

put
,
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const asVoid: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, E, R>

This function maps the success value of an Effect value to void. If the original Effect value succeeds, the returned Effect value will also succeed. If the original Effect value fails, the returned Effect value will fail with the same error.

@since2.0.0

asVoid
)
)
)
const
const sonnet: Sonnet.Sonnet<never, never>
sonnet
=
import Sonnet
Sonnet
.
const make: <Sonnet.SonnetService, never>(rootEffect: Effect.Effect<void, never, Sonnet.SonnetService>, layer: Layer<Sonnet.SonnetService, never, never>, memoMap?: MemoMap | undefined) => Sonnet.Sonnet<...>

Construct a redux-sonnet middleware.

NOTE: side-effect handlers are provided up-front to guarantee declarative behavior.

@example

import { configureStore, Tuple as RTKTuple } from "@reduxjs/toolkit"
import { Sonnet, Stanza } from "redux-sonnet"
import { Stream, Layer } from "effect"
const stanza = Stanza.fromStream(Stream.make(
{ type: "ACTION-1" },
{ type: "ACTION-2" },
{ type: "ACTION-3" },
))
const sonnet = Sonnet.make(
// ^?
stanza,
Sonnet.defaultLayer,
)
const store = configureStore({
// ^?
reducer: () => {},
middleware: () => new RTKTuple(sonnet)
})

@since0.0.0

make
(
const stanza: Effect.Effect<void, never, Sonnet.SonnetService>
stanza
,
import Sonnet
Sonnet
.
const defaultLayer: Layer<Sonnet.SonnetService, never, never>

@since0.0.0

defaultLayer
)