Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

Data fetching

Share, dedupe, and refresh fetches with an Effect Cache and atoms.

Ilha has no fetch helper. You put an Effect Cache in a module and read it through an atom. Concurrent reads of one key share one request; later reads hit the cache until the entry expires.

import * as import CacheCache from "effect/Cache";
import * as import EffectEffect from "effect/Effect";
import * as import AtomAtom from "effect/reactivity/Atom";
import { const atom: AtomFnatom } from "ilha";

interface User {
  readonly User.name: stringname: string;
}

// One cache per module: every component on the page shares it.
const const users: Cache.Cache<string, User, UnknownError, never>users = import EffectEffect.const runSync: <Cache.Cache<string, User, UnknownError, never>, never>(effect: Effect.Effect<Cache.Cache<string, User, UnknownError, never>, never, never>) => Cache.Cache<string, User, UnknownError, never>
Executes an effect synchronously and returns its success value. **When to use** Use when you need to execute an effect that is guaranteed to complete synchronously. **Details** If the effect fails, dies, is interrupted, or performs asynchronous work, `runSync` throws a `FiberFailure` instead of returning a value. Use `runSyncExit` when you want the failure captured as an `Exit`. **Example** (Running a synchronous effect) ```ts import.meta.vitest import { Effect } from "effect" const output: Array<unknown> = [] const program = Effect.sync(() => { void output.push("Hello, World!") return 1 }) const result = Effect.runSync(program) void output.push(result) output // => ["Hello, World!", 1] ``` **Example** (Throwing for failed or async effects) ```ts import.meta.vitest import { Effect } from "effect" const output: Array<unknown> = [] try { // Attempt to run an effect that fails Effect.runSync(Effect.fail("my error")) } catch (e) { void output.push("failed effect") } try { // Attempt to run an effect that involves async work Effect.runSync(Effect.promise(() => Promise.resolve(1))) } catch (e) { void output.push("async effect") } output // => ["failed effect", "async effect"] ```
@see{@link runSyncExit} for a version that returns an `Exit` type instead of throwing an error.@categoryrunning@since2.0.0
runSync
(
import CacheCache.
const make: <string, User, UnknownError, never, never>(options: {
    readonly lookup: (key: string) => Effect.Effect<User, UnknownError, never>;
    readonly capacity: number;
    readonly timeToLive?: Input | undefined;
    readonly requireServicesAt?: undefined;
}) => Effect.Effect<Cache.Cache<string, User, UnknownError, never>, never, never>
Creates a cache with a fixed time-to-live for all entries. **Details** This is the basic cache constructor where all entries share the same TTL. The lookup function will be called when a key is not found or has expired. **Example** (Creating a basic cache) ```ts import.meta.vitest import { Cache, Effect } from "effect" // Basic cache with string keys const program = Effect.gen(function*() { const cache = yield* Cache.make<string, number>({ capacity: 100, lookup: (key) => Effect.succeed(key.length) }) const result1 = yield* Cache.get(cache, "hello") const result2 = yield* Cache.get(cache, "world") return { result1, result2 } }) const actual = await Effect.runPromise(program) actual // => { result1: 5, result2: 5 } ``` **Example** (Creating a cache with TTL) ```ts import.meta.vitest import { Cache, Effect } from "effect" const program = Effect.gen(function*() { const users = new Map([ [123, { name: "Ada", email: "ada@example.com" }], [456, { name: "Grace", email: "grace@example.com" }] ]) const cache = yield* Cache.make< number, { name: string; email: string }, string >({ capacity: 500, lookup: (userId) => Effect.suspend(() => { const user = users.get(userId) return user === undefined ? Effect.fail(`User ${userId} not found`) : Effect.succeed(user) }), timeToLive: "15 minutes" }) const user1 = yield* Cache.get(cache, 123) const user2 = yield* Cache.get(cache, 123) return [user1, user2, user1 === user2] as const }) const actual = await Effect.runPromise(program) actual // => [{ name: "Ada", email: "ada@example.com" }, { name: "Ada", email: "ada@example.com" }, true] ```
@categoryconstructors@since2.0.0
make
({
capacity: numbercapacity: 100, lookup: (key: string) => Effect.Effect<User, UnknownError, never>lookup: (id: stringid: string) => import EffectEffect.const tryPromise: <User>(options: (signal: AbortSignal) => PromiseLike<User>) => Effect.Effect<User, UnknownError, never> (+1 overload)
Creates an `Effect` from an asynchronous computation that may throw or reject, mapping failures into the error channel. **When to use** Use when you need to perform asynchronous operations that might fail, such as fetching data from an API, and want thrown exceptions or rejected promises captured as Effect errors. **Details** The promise thunk is evaluated when the effect runs. If it returns a promise that resolves, the resolved value becomes the success value. If the thunk throws before returning a promise, or if the returned promise rejects, the thrown or rejected value is mapped into the error channel. Passing the thunk directly maps failures to {@link Cause.UnknownError } . Passing `{ try, catch }` uses `catch` to map failures to an error of type `E`. The thunk receives an `AbortSignal` that is aborted if the effect is interrupted. The underlying asynchronous operation only stops if it observes that signal. **Gotchas** If `catch` throws while mapping the error, that thrown value is treated as a defect. Return the error value you want in the error channel instead of throwing it. **Example** (Wrapping a fetch request that may fail) ```ts import.meta.vitest import { Effect } from "effect" const getTodo = (id: number) => Effect.tryPromise(() => Promise.resolve({ id, completed: false })) // ┌─── Effect<{ id: number; completed: boolean }, UnknownError, never> // ▼ const program = getTodo(1) await Effect.runPromise(program) // => { id: 1, completed: false } ``` **Example** (Mapping Promise rejections to a tagged error) ```ts import.meta.vitest import { Data, Effect } from "effect" class TodoFetchError extends Data.TaggedError("TodoFetchError")<{ readonly cause: unknown }> {} const getTodo = (id: number) => Effect.tryPromise({ try: () => Promise.reject(`Todo ${id} is unavailable`), // remap the error catch: (cause) => new TodoFetchError({ cause }) }) // ┌─── Effect<never, TodoFetchError, never> // ▼ const program = Effect.flip(getTodo(1)) const error = await Effect.runPromise(program) error._tag // => "TodoFetchError" ```
@see{@link promise} if the effectful computation is asynchronous and does not throw errors.@categoryconstructors@since2.0.0
tryPromise
(async (): interface Promise<T>
Represents the completion of an asynchronous operation
Promise
<User> => {
const const res: Responseres = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
fetch
(`/api/users/${id: stringid}`);
return const res: Responseres.Body.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)
json
();
}), timeToLive?: Input | undefinedtimeToLive: "1 minute", }) ); export const
const UserCard: ({ id }: {
    id: string;
}) => View
UserCard
= ({ id: stringid }: { id: stringid: string }) => {
const const user: AtomHandle<AsyncResult<View, UnknownError>>user = atom<View, UnknownError>(init: Effect.Effect<View, UnknownError, AtomRegistry> | Stream<View, UnknownError, AtomRegistry>, options?: AtomOptions<AsyncResult<View, UnknownError>> | undefined): AtomHandle<AsyncResult<View, UnknownError>> (+1 overload)
An effect or stream runs once; the handle holds its `AsyncResult`.
atom
(
import CacheCache.const get: <string, User, UnknownError, never>(self: Cache.Cache<string, User, UnknownError, never>, key: string) => Effect.Effect<User, UnknownError, never> (+1 overload)
Retrieves the value for a key, invoking the lookup function on a cache miss or expired entry. **Details** Concurrent `get` calls for the same missing key share the same pending lookup. The cache stores the lookup `Exit`, so failed lookups are cached and will fail again until the entry expires, is invalidated, or is refreshed. **Example** (Getting cached values) ```ts import.meta.vitest import { Cache, Effect } from "effect" const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) }) // Cache miss - triggers lookup function const result1 = yield* Cache.get(cache, "hello") // Cache hit - returns cached value without lookup const result2 = yield* Cache.get(cache, "hello") return { result1, result2 } }) const actual = await Effect.runPromise(program) actual // => { result1: 5, result2: 5 } ``` **Example** (Handling lookup failures) ```ts import.meta.vitest import { Cache, Effect, Exit } from "effect" // Error handling when lookup fails const program = Effect.gen(function*() { const cache = yield* Cache.make<string, number, string>({ capacity: 10, lookup: (key: string) => key === "error" ? Effect.fail("Lookup failed") : Effect.succeed(key.length) }) // Successful lookup const success = yield* Cache.get(cache, "hello") // Failed lookup - returns error const failure = yield* Effect.exit(Cache.get(cache, "error")) return [success, failure] as const }) const actual = await Effect.runPromise(program) actual // => [5, Exit.fail("Lookup failed")] ``` **Example** (Sharing concurrent lookups) ```ts import.meta.vitest import { Cache, Effect } from "effect" // Concurrent access - multiple gets of same key only invoke lookup once const program = Effect.gen(function*() { let lookupCount = 0 const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.sync(() => { lookupCount++ return key.length }) }) // Multiple concurrent gets const results = yield* Effect.all([ Cache.get(cache, "hello"), Cache.get(cache, "hello"), Cache.get(cache, "hello") ], { concurrency: "unbounded" }) return { results, lookupCount } }) const actual = await Effect.runPromise(program) actual // => { results: [5, 5, 5], lookupCount: 1 } ```
@categorycombinators@since4.0.0
get
(const users: Cache.Cache<string, User, UnknownError, never>users, id: stringid).Pipeable.pipe<Effect.Effect<User, UnknownError, never>, Effect.Effect<View, UnknownError, never>>(this: Effect.Effect<User, UnknownError, never>, ab: (_: Effect.Effect<User, UnknownError, never>) => Effect.Effect<View, UnknownError, never>): Effect.Effect<View, UnknownError, never> (+21 overloads)pipe(import EffectEffect.const map: <User, View>(f: (a: User) => View) => <E, R>(self: Effect.Effect<User, E, R>) => Effect.Effect<View, E, R> (+1 overload)
Transforms the value inside an effect by applying a function to it. **When to use** Use to transform an effect's success value with a function that returns a plain value, producing a new effect without changing the original effect's typed error or context requirements. **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. **Example** (Choosing map syntax variants) ```ts import.meta.vitest import { Effect, pipe } from "effect" const output: Array<unknown> = [] const myEffect = Effect.succeed(1) const transformation = (n: number) => n + 1 const mappedWithPipe = pipe(myEffect, Effect.map(transformation)) const mappedWithDataFirst = Effect.map(myEffect, transformation) const mappedWithMethod = myEffect.pipe(Effect.map(transformation)) void output.push(Effect.runSync(Effect.all([ mappedWithPipe, mappedWithDataFirst, mappedWithMethod ]))) output // => [[2, 2, 2]] ``` **Example** (Adding a service charge) ```ts import.meta.vitest import { Effect, pipe } from "effect" const addServiceCharge = (amount: number) => amount + 1 const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) const finalAmount = pipe( fetchTransactionAmount, Effect.map(addServiceCharge) ) await Effect.runPromise(finalAmount) // => 101 ```
@see{@link mapError} for a version that operates on the error channel.@see{@link mapBoth} for a version that operates on both channels.@see{@link flatMap} or {@link andThen} for a version that can return a new effect.@categorymapping@since2.0.0
map
((u: Useru) => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{u: Useru.User.name: stringname}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>))
); return ( <JSX.IntrinsicElements.section: HTMLAttributes<HTMLElement>section> {const user: AtomHandle<AsyncResult<View, UnknownError>>user} <JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton type?: string | undefinedtype="button" EventProps<HTMLButtonElement>.onclick?: ((event: Targeted<HTMLButtonElement, MouseEvent>) => void) | undefinedonclick={() => import CacheCache.const invalidate: <string, User, UnknownError, never>(self: Cache.Cache<string, User, UnknownError, never>, key: string) => Effect.Effect<void> (+1 overload)
Invalidates the entry associated with the specified key in the cache. **Example** (Invalidating cached entries) ```ts import.meta.vitest import { Cache, Effect } from "effect" const program = Effect.gen(function*() { const cache = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.succeed(key.length) }) // Add a value to the cache yield* Cache.get(cache, "hello") const beforeInvalidation = yield* Cache.has(cache, "hello") // Invalidate the entry yield* Cache.invalidate(cache, "hello") const afterInvalidation = yield* Cache.has(cache, "hello") // Invalidating non-existent keys doesn't error yield* Cache.invalidate(cache, "nonexistent") // Get after invalidation will invoke lookup again let lookupCount = 0 const cache2 = yield* Cache.make({ capacity: 10, lookup: (key: string) => Effect.sync(() => { lookupCount++ return key.length }) }) yield* Cache.get(cache2, "test") // lookupCount = 1 yield* Cache.invalidate(cache2, "test") yield* Cache.get(cache2, "test") // lookupCount = 2 (lookup called again) return { beforeInvalidation, afterInvalidation, lookupCount } }) const actual = await Effect.runPromise(program) actual // => { beforeInvalidation: true, afterInvalidation: false, lookupCount: 2 } ```
@categorycombinators@since4.0.0
invalidate
(const users: Cache.Cache<string, User, UnknownError, never>users, id: stringid).Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, never, AtomRegistry>>(this: Effect.Effect<void, never, never>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, never, AtomRegistry>): Effect.Effect<void, never, AtomRegistry> (+21 overloads)pipe(
import EffectEffect.const andThen: <void, never, AtomRegistry>(f: Effect.Effect<void, never, AtomRegistry>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, E, AtomRegistry | R> (+3 overloads)
Runs this effect and then runs another effect, optionally using the first effect's success value to choose the next effect. **When to use** Use when you need one effect to run after another and the second effect may depend on the first effect's success value. **Details** When the second argument is an `Effect`, the first success value is discarded and the returned effect produces the second effect's value. When the second argument is a function, it receives the first success value and must return the next `Effect`. Failures or requirements from either effect are preserved in the returned effect. **Example** (Choosing andThen syntax variants) ```ts import.meta.vitest import { Effect, pipe } from "effect" const output: Array<unknown> = [] const myEffect = Effect.succeed(1) const anotherEffect = Effect.succeed("done") const transformedWithPipe = pipe(myEffect, Effect.andThen(anotherEffect)) const transformedWithDataFirst = Effect.andThen(myEffect, anotherEffect) const transformedWithMethod = myEffect.pipe(Effect.andThen(anotherEffect)) void output.push(Effect.runSync(Effect.all([ transformedWithPipe, transformedWithDataFirst, transformedWithMethod ]))) output // => [['done', 'done', 'done']] ``` **Example** (Sequencing a discount calculation after fetching a total) ```ts import.meta.vitest import { Data, Effect, pipe } from "effect" class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} // Function to apply a discount safely to a transaction amount const applyDiscount = ( total: number, discountRate: number ): Effect.Effect<number, DiscountRateError> => discountRate === 0 ? Effect.fail(new DiscountRateError()) : Effect.succeed(total - (total * discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from database const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100)) // Using Effect.map and Effect.flatMap const result1 = pipe( fetchTransactionAmount, Effect.map((amount) => amount * 2), Effect.flatMap((amount) => applyDiscount(amount, 5)) ) await Effect.runPromise(result1) // => 190 // Using Effect.andThen const result2 = pipe( fetchTransactionAmount, Effect.andThen((amount) => Effect.succeed(amount * 2)), Effect.andThen((amount) => applyDiscount(amount, 5)) ) await Effect.runPromise(result2) // => 190 ```
@categorysequencing@since2.0.0
andThen
(import AtomAtom.const refresh: <AsyncResult<View, UnknownError>>(self: Atom.Atom<AsyncResult<View, UnknownError>>) => Effect.Effect<void, never, AtomRegistry>
Runs a refresh request for an atom through the `AtomRegistry` service. **When to use** Use to invalidate and recompute an atom from an Effect that has access to the active registry.
@stabilityunstable@categoryconverting@since4.0.0
refresh
(const user: AtomHandle<AsyncResult<View, UnknownError>>user.AtomHandle<AsyncResult<View, UnknownError>>.atom: Atom.Atom<AsyncResult<View, UnknownError>>atom))
) } > Refresh </JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton> </JSX.IntrinsicElements.section: HTMLAttributes<HTMLElement>section> ); };
  1. atom(effect) runs the lookup and holds an AsyncResult.
  2. {user} paints nothing while waiting, the view on success, and the error message on failure. A refresh keeps the previous view until the new one lands.
  3. The click handler returns an Effect. Ilha runs it with the component’s atom registry, so Atom.refresh reruns the lookup.

Read the result in the component

Read the handle to branch on the state yourself:

import * as import EffectEffect from "effect/Effect";
import * as import AsyncResultAsyncResult from "effect/reactivity/AsyncResult";
import { const atom: AtomFnatom } from "ilha";

const const loadCount: Effect.Effect<number, UnknownError, never>loadCount = import EffectEffect.const tryPromise: <number>(options: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, UnknownError, never> (+1 overload)
Creates an `Effect` from an asynchronous computation that may throw or reject, mapping failures into the error channel. **When to use** Use when you need to perform asynchronous operations that might fail, such as fetching data from an API, and want thrown exceptions or rejected promises captured as Effect errors. **Details** The promise thunk is evaluated when the effect runs. If it returns a promise that resolves, the resolved value becomes the success value. If the thunk throws before returning a promise, or if the returned promise rejects, the thrown or rejected value is mapped into the error channel. Passing the thunk directly maps failures to {@link Cause.UnknownError } . Passing `{ try, catch }` uses `catch` to map failures to an error of type `E`. The thunk receives an `AbortSignal` that is aborted if the effect is interrupted. The underlying asynchronous operation only stops if it observes that signal. **Gotchas** If `catch` throws while mapping the error, that thrown value is treated as a defect. Return the error value you want in the error channel instead of throwing it. **Example** (Wrapping a fetch request that may fail) ```ts import.meta.vitest import { Effect } from "effect" const getTodo = (id: number) => Effect.tryPromise(() => Promise.resolve({ id, completed: false })) // ┌─── Effect<{ id: number; completed: boolean }, UnknownError, never> // ▼ const program = getTodo(1) await Effect.runPromise(program) // => { id: 1, completed: false } ``` **Example** (Mapping Promise rejections to a tagged error) ```ts import.meta.vitest import { Data, Effect } from "effect" class TodoFetchError extends Data.TaggedError("TodoFetchError")<{ readonly cause: unknown }> {} const getTodo = (id: number) => Effect.tryPromise({ try: () => Promise.reject(`Todo ${id} is unavailable`), // remap the error catch: (cause) => new TodoFetchError({ cause }) }) // ┌─── Effect<never, TodoFetchError, never> // ▼ const program = Effect.flip(getTodo(1)) const error = await Effect.runPromise(program) error._tag // => "TodoFetchError" ```
@see{@link promise} if the effectful computation is asynchronous and does not throw errors.@categoryconstructors@since2.0.0
tryPromise
(async () => {
const const res: Responseres = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
fetch
("/api/count");
return
var Number: NumberConstructor
(value?: any) => number
An object that represents a number of any kind. All JavaScript numbers are 64-bit floating-point numbers.
Number
(await const res: Responseres.Body.text(): Promise<string>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text)
text
());
}); export const const Count: () => ViewCount = () => { const const count: AtomHandle<AsyncResult.AsyncResult<number, UnknownError>>count = atom<number, UnknownError>(init: Effect.Effect<number, UnknownError, AtomRegistry> | Stream<number, UnknownError, AtomRegistry>, options?: AtomOptions<AsyncResult.AsyncResult<number, UnknownError>> | undefined): AtomHandle<AsyncResult.AsyncResult<number, UnknownError>> (+1 overload)
An effect or stream runs once; the handle holds its `AsyncResult`.
atom
(const loadCount: Effect.Effect<number, UnknownError, never>loadCount);
return import AsyncResultAsyncResult.
const match: <number, UnknownError, View, View, View>(self: AsyncResult.AsyncResult<number, UnknownError>, options: {
    readonly onInitial: (_: AsyncResult.Initial<number, UnknownError>) => View;
    readonly onFailure: (_: AsyncResult.Failure<number, UnknownError>) => View;
    readonly onSuccess: (_: AsyncResult.Success<number, UnknownError>) => View;
}) => View (+1 overload)
Pattern matches an `AsyncResult` by calling the handler for `Initial`, `Failure`, or `Success`.
@stabilityunstable@categorycombinators@since4.0.0
match
(
const count: AtomHandle
() => AsyncResult.AsyncResult<number, UnknownError>
count
(), {
onFailure: (_: AsyncResult.Failure<number, UnknownError>) => ViewonFailure: () => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>Could not load</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>, onInitial: (_: AsyncResult.Initial<number, UnknownError>) => ViewonInitial: () => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>Loading</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>, onSuccess: (_: AsyncResult.Success<number, UnknownError>) => ViewonSuccess: ({ value: numbervalue }) => <JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{value: numbervalue}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>, }); };

Server rendering

renderToString does not wait for these lookups. Load data that must be in the HTML on the server, then pass it in as props. With Void, page loaders do this for you.

Was this page helpful?