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"]
```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]
```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"
```tryPromise(async (): interface Promise<T>Represents the completion of an asynchronous operationPromise<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 }
```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
```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 }
```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
```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.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>
);
};
atom(effect)runs the lookup and holds anAsyncResult.{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.- The click handler returns an Effect. Ilha runs it with the component’s atom registry, so
Atom.refreshreruns 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"
```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`.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.