Skip to content

AsyncResult

AsyncResult<A, E> is the state container that effect-backed and stream-backed atoms expose. It records the latest observable state of work that may still be loading, refreshing, retrying, or recovering from failure. The value is always one of three variants:

  • Initial — nothing has been computed yet.
  • Success — a value A is available, along with its timestamp.
  • Failure — a Cause<E> is available, and the latest previous Success may be carried alongside it.

Every variant also carries a waiting flag, so you can keep rendering the current state while newer work is in flight.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
// The three variants, all typed AsyncResult<number, string>
const a = AsyncResult.initial<number, string>()
const b = AsyncResult.success(42)
const c = AsyncResult.fail<string, number>("boom")
a._tag // => "Initial"
b._tag // => "Success"
c._tag // => "Failure"

The variant answers “what do we know right now?”, while waiting answers “is newer work currently running?”. This split is what makes AsyncResult different from Exit or Result:

  • It has an Initial state for “nothing has happened yet” (no value, no error) — neither Exit nor Result can express this.
  • waiting is an overlay, not a fourth variant. A Success can be waiting (refreshing in the background), and so can a Failure.
  • A Failure can keep the previous success (previousSuccess). This lets UI code show stale data while exposing the latest failure for error displays and retry logic — the “smooth refresh” experience.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
// A success that is being refreshed: value is still here, but waiting is true.
const refreshing = AsyncResult.waiting(AsyncResult.success(42))
refreshing.waiting // => true
AsyncResult.value(refreshing) // => Option.some(42)
// A failure that carries the previous success forward.
const failed = AsyncResult.failWithPrevious("network down", {
previous: Option.some(AsyncResult.success(42))
})
AsyncResult.value(failed) // => Option.some(42) (stale, but renderable)
AsyncResult.error(failed) // => Option.some("network down")

The usual job is turning an AsyncResult into UI. Use matchWithWaiting when “is it loading?” should win over the underlying variant — it handles waiting (and Initial) first, then splits a Failure into typed errors vs. unexpected defects:

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
type User = { name: string }
const render = (result: AsyncResult.AsyncResult<User, string>): string =>
AsyncResult.matchWithWaiting(result, {
onWaiting: () => "Loading…",
onError: (error) => `Error: ${error}`,
onDefect: (defect) => `Unexpected: ${String(defect)}`,
onSuccess: (success) => `Hello, ${success.value.name}`
})
render(AsyncResult.initial()) // => "Loading…"
render(AsyncResult.waiting(AsyncResult.success({ name: "Ada" }))) // => "Loading…"
render(AsyncResult.success({ name: "Ada" })) // => "Hello, Ada"
render(AsyncResult.fail("offline")) // => "Error: offline"

If instead you want the variant to win regardless of waiting, use match, which exposes Initial / Failure / Success directly.


The three-state union: Initial<A, E> | Success<A, E> | Failure<A, E>. E defaults to never.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
let result: AsyncResult.AsyncResult<number, string>
result = AsyncResult.initial()
result = AsyncResult.success(1)
result = AsyncResult.fail("nope")

Returns true when an unknown value is an AsyncResult.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isAsyncResult(AsyncResult.success(1)) // => true
AsyncResult.isAsyncResult({ _tag: "Success" }) // => false

Type-level helper that rebuilds an AsyncResult with new A/E types while preserving the variant of another result type. Used by combinators like map and replacePrevious to keep the variant precise.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
// Given a Success, With keeps it a Success with new type params.
type R = AsyncResult.With<AsyncResult.Success<number>, string, never>
// => AsyncResult.Success<string, never>

The AsyncResult.AsyncResult namespace holds type-level helpers shared by every variant:

  • AsyncResult.Proto<A, E> — the common prototype: pipeability, the [TypeId] marker, the phantom A/E members, and the waiting: boolean flag implemented by all three variants.
  • AsyncResult.Success<R> — extracts the success value type from an AsyncResult type.
  • AsyncResult.Failure<R> — extracts the failure error type.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
type R = AsyncResult.AsyncResult<number, string>
type A = AsyncResult.AsyncResult.Success<R> // => number
type E = AsyncResult.AsyncResult.Failure<R> // => string

The state before any success value or failure cause exists. Has only _tag and the inherited waiting flag.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const init = AsyncResult.initial<number, string>()
init._tag // => "Initial"
init.waiting // => false

A successful result. Carries the current value, a timestamp (ms, when the value was produced), and the shared waiting flag.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const ok = AsyncResult.success(42)
ok._tag // => "Success"
ok.value // => 42
typeof ok.timestamp // => "number"

A failed result. Carries a cause: Cause<E> and previousSuccess: Option<Success<A, E>> — the most recent success carried forward (if any).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
const f = AsyncResult.fail<string, number>("boom")
f._tag // => "Failure"
f.previousSuccess // => Option.none()
Option.isOption(f.previousSuccess) // => true

Phantom type markers used only by the builder to track, at the type level, whether the defect and interrupt failure cases still need to be handled before exhaustive() becomes available. They have no runtime values.

Returns whether a result has its waiting flag set (any variant can be waiting).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isWaiting(AsyncResult.initial(true)) // => true
AsyncResult.isWaiting(AsyncResult.success(1)) // => false
AsyncResult.isWaiting(AsyncResult.waiting(AsyncResult.success(1))) // => true

Type guard narrowing to Initial<A, E>.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isInitial(AsyncResult.initial()) // => true
AsyncResult.isInitial(AsyncResult.success(1)) // => false

Type guard narrowing to Success<A, E> | Failure<A, E> (anything that has “settled” at least once).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isNotInitial(AsyncResult.success(1)) // => true
AsyncResult.isNotInitial(AsyncResult.initial()) // => false

Type guard narrowing to Success<A, E>.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isSuccess(AsyncResult.success(1)) // => true
AsyncResult.isSuccess(AsyncResult.fail("x")) // => false

Type guard narrowing to Failure<A, E>.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isFailure(AsyncResult.fail("x")) // => true
AsyncResult.isFailure(AsyncResult.success(1)) // => false

Returns true only for a Failure whose cause contains only interruptions (e.g. the work was cancelled). Useful for treating cancellation differently from real errors.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Cause } from "effect"
AsyncResult.isInterrupted(AsyncResult.failure(Cause.interrupt(1))) // => true
AsyncResult.isInterrupted(AsyncResult.fail("boom")) // => false

Creates an Initial result, optionally marking it as waiting.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.initial().waiting // => false
AsyncResult.initial(true).waiting // => true

Creates a Success from a value. Options can set waiting and override the timestamp (defaults to Date.now()).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const a = AsyncResult.success(42)
a.value // => 42
const b = AsyncResult.success(42, { waiting: true, timestamp: 1000 })
b.waiting // => true
b.timestamp // => 1000

Creates a Failure from a Cause<E>. Options can attach a previousSuccess and set waiting.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Cause } from "effect"
const f = AsyncResult.failure(Cause.fail("boom"))
f._tag // => "Failure"
AsyncResult.error(f) // => Option.some("boom")

Convenience for failure from a typed error — wraps it in Cause.fail.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const f = AsyncResult.fail("not found")
AsyncResult.error(f) // => Option.some("not found")

Converts an Exit into a Success (when it succeeds) or a Failure carrying the exit’s cause (when it fails).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Exit } from "effect"
AsyncResult.fromExit(Exit.succeed(7))._tag // => "Success"
AsyncResult.fromExit(Exit.fail("x"))._tag // => "Failure"

Like fromExit, but when the exit is a failure it carries forward the latest success from a previous result — the key move for smooth refresh.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Exit, Option } from "effect"
const prev = Option.some(AsyncResult.success(42))
// Refresh succeeds: new success.
AsyncResult.fromExitWithPrevious(Exit.succeed(43), prev).value // => 43
// Refresh fails: failure keeps the stale value.
const f = AsyncResult.fromExitWithPrevious(Exit.fail("offline"), prev)
AsyncResult.value(f) // => Option.some(42)
AsyncResult.error(f) // => Option.some("offline")

Creates a Failure from a Cause, pulling the latest success out of a previous result (whether that previous result was itself a Success or a Failure that already carried one).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Cause, Option } from "effect"
const prev = Option.some(AsyncResult.success(42))
const f = AsyncResult.failureWithPrevious(Cause.fail("boom"), { previous: prev })
AsyncResult.value(f) // => Option.some(42)

Like failureWithPrevious, but takes a typed error instead of a Cause.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
const prev = Option.some(AsyncResult.success(42))
const f = AsyncResult.failWithPrevious("boom", { previous: prev })
AsyncResult.value(f) // => Option.some(42)
AsyncResult.error(f) // => Option.some("boom")

Marks any result as waiting (returns it unchanged if already waiting). Pass { touch: true } to also refresh a Success’s timestamp.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const ok = AsyncResult.success(42)
const refreshing = AsyncResult.waiting(ok)
refreshing.waiting // => true
refreshing.value // => 42 (preserved)

Builds a waiting result from an optional previous result: waiting(previous) when one exists, or initial(true) when none does. This is exactly the transition an atom makes when it starts loading.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
AsyncResult.waitingFrom(Option.none())._tag // => "Initial"
AsyncResult.waitingFrom(Option.none()).waiting // => true
const w = AsyncResult.waitingFrom(Option.some(AsyncResult.success(42)))
w._tag // => "Success"
w.waiting // => true

Refreshes the timestamp of a Success (preserving value and waiting). Non-success results are returned unchanged.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const old = AsyncResult.success(42, { timestamp: 1000 })
const fresh = AsyncResult.touch(old)
fresh.value // => 42
fresh.timestamp // => Date.now() (refreshed, no longer 1000)

Swaps the previousSuccess stored inside a Failure for the latest success found in another result. Non-failures pass through unchanged.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
const f = AsyncResult.fail<string, number>("boom")
const withPrev = AsyncResult.replacePrevious(
f,
Option.some(AsyncResult.success(99))
)
AsyncResult.value(withPrev) // => Option.some(99)

Maps the success value. Also maps the previousSuccess carried inside a Failure; Initial is left unchanged.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.map(AsyncResult.success(2), (n) => n * 10).value // => 20
AsyncResult.map(AsyncResult.initial<number>(), (n) => n * 10)._tag // => "Initial"
// Pipeable form
AsyncResult.success(2).pipe(AsyncResult.map((n) => n + 1)).value // => 3

Maps the success value to another AsyncResult and flattens. Initial passes through; a Failure keeps its cause and remaps any stored previous success. The mapping function also receives the originating Success.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const r = AsyncResult.flatMap(AsyncResult.success(5), (n) =>
n > 0 ? AsyncResult.success(n * 2) : AsyncResult.fail("non-positive")
)
r.value // => 10
AsyncResult.flatMap(AsyncResult.initial<number, string>(), () =>
AsyncResult.success(1)
)._tag // => "Initial"

Pattern matches on the variant directly: onInitial, onFailure, onSuccess. Use this when the variant should win over waiting.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const describe = (r: AsyncResult.AsyncResult<number, string>) =>
AsyncResult.match(r, {
onInitial: () => "initial",
onFailure: (f) => `failure (waiting=${f.waiting})`,
onSuccess: (s) => `success: ${s.value}`
})
describe(AsyncResult.success(7)) // => "success: 7"
describe(AsyncResult.initial()) // => "initial"

Like match, but splits a Failure into a typed onError (the first typed E in the cause) vs. onDefect (a squashed non-error cause, e.g. an unexpected throw or interrupt).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Cause } from "effect"
const handle = (r: AsyncResult.AsyncResult<number, string>) =>
AsyncResult.matchWithError(r, {
onInitial: () => "initial",
onError: (error) => `error: ${error}`,
onDefect: (defect) => `defect: ${String(defect)}`,
onSuccess: (s) => `ok: ${s.value}`
})
handle(AsyncResult.fail("boom")) // => "error: boom"
handle(AsyncResult.failure(Cause.die("kaboom"))) // => "defect: kaboom"

The UI-oriented matcher: onWaiting runs for any waiting result and for Initial, before the variant branches. Otherwise behaves like matchWithError (typed onError vs. onDefect).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const view = (r: AsyncResult.AsyncResult<number, string>) =>
AsyncResult.matchWithWaiting(r, {
onWaiting: () => "spinner",
onError: (e) => `error: ${e}`,
onDefect: (d) => `crash: ${String(d)}`,
onSuccess: (s) => `value: ${s.value}`
})
view(AsyncResult.initial()) // => "spinner"
view(AsyncResult.waiting(AsyncResult.success(1))) // => "spinner"
view(AsyncResult.success(1)) // => "value: 1"
view(AsyncResult.fail("boom")) // => "error: boom"

Combines an iterable or record of AsyncResults (and plain values) into a single AsyncResult. Returns the first non-success result as-is, or a Success of the collected values — marked waiting if any combined success was waiting.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
// Record: collected into a success of the same shape.
const r = AsyncResult.all({
user: AsyncResult.success("Ada"),
age: AsyncResult.success(36),
plan: "free" // plain value passes through
})
r._tag // => "Success"
r.value // => { user: "Ada", age: 36, plan: "free" }
// Short-circuits on the first non-success.
AsyncResult.all([
AsyncResult.success(1),
AsyncResult.fail("boom"),
AsyncResult.success(3)
])._tag // => "Failure"

Converts a result to an Exit: succeed for Success, fail the cause for Failure, and fail with NoSuchElementError for Initial.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Exit } from "effect"
Exit.isSuccess(AsyncResult.toExit(AsyncResult.success(1))) // => true
Exit.isFailure(AsyncResult.toExit(AsyncResult.initial())) // => true (NoSuchElementError)

Returns the current success value, or the previous success stored in a failure, as an Option. Returns None for Initial and for failures with no previous success.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Option } from "effect"
AsyncResult.value(AsyncResult.success(42)) // => Option.some(42)
AsyncResult.value(AsyncResult.initial()) // => Option.none()
const f = AsyncResult.failWithPrevious("boom", {
previous: Option.some(AsyncResult.success(42))
})
AsyncResult.value(f) // => Option.some(42) (stale value still readable)

Returns the value from value, or evaluates a lazy fallback when no current or previous success exists.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.getOrElse(AsyncResult.success(42), () => 0) // => 42
AsyncResult.getOrElse(AsyncResult.initial<number>(), () => 0) // => 0
// Pipeable form
AsyncResult.initial<number>().pipe(AsyncResult.getOrElse(() => -1)) // => -1

Like getOrElse but throws NoSuchElementError when no current or previous success exists.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.getOrThrow(AsyncResult.success(42)) // => 42
AsyncResult.getOrThrow(AsyncResult.initial()) // => throws NoSuchElementError

Returns the failure Cause as an OptionSome only for a Failure.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.cause(AsyncResult.fail("boom")) // => Option.some(Cause.fail("boom"))
AsyncResult.cause(AsyncResult.success(1)) // => Option.none()

Returns the first typed error E from a failure cause. Returns None for successes, initials, defects, and interrupt-only causes.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Cause } from "effect"
AsyncResult.error(AsyncResult.fail("boom")) // => Option.some("boom")
AsyncResult.error(AsyncResult.failure(Cause.die("oops"))) // => Option.none() (defect)
AsyncResult.error(AsyncResult.success(1)) // => Option.none()

Creates a fluent, type-tracked renderer for an AsyncResult. Each handler (onWaiting, onInitial, onSuccess, onError, onErrorTag, onErrorIf, onDefect, onInterrupt, onFailure) narrows the set of remaining cases at the type level. exhaustive() only becomes callable once every possible case is handled; otherwise finish with orElse, orNull, or render.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
const result = AsyncResult.success(42) as AsyncResult.AsyncResult<number, string>
const rendered = AsyncResult.builder(result)
.onInitialOrWaiting(() => "Loading…")
.onSuccess((value) => `Value: ${value}`)
// onFailure handles the whole cause (typed errors, defects, and interrupts),
// which is what makes the remaining cases empty and exhaustive() callable.
.onFailure((cause) => `Failed: ${String(cause)}`)
.exhaustive()
// => "Value: 42"

The builder shines when an error channel is a union of tagged errors — handle each tag (which narrows E) and the compiler tracks what’s left:

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
type AppError =
| { readonly _tag: "NotFound" }
| { readonly _tag: "Forbidden" }
const result = AsyncResult.fail<AppError, string>({
_tag: "NotFound"
}) as AsyncResult.AsyncResult<string, AppError>
const text = AsyncResult.builder(result)
.onWaiting(() => "…")
.onSuccess((value) => value)
.onErrorTag("NotFound", () => "Not found")
.onErrorTag("Forbidden", () => "Forbidden")
.orElse(() => "Unknown")
// => "Not found"

The type behind builder. It is a Pipeable that exposes case-handler methods conditionally based on which cases are still unhandled — onSuccess disappears once success is handled, onErrorTag narrows E, and exhaustive() only appears when the remaining A, E, Initial, defect, and interrupt slots are all never. Terminal methods:

  • orElse(f) — produce the accumulated output or evaluate the fallback.
  • orNull() — produce the output or null.
  • render() — produce the output, or null for an unhandled Initial; throws the squashed cause for an unhandled Failure.
  • exhaustive() — produce the output, available only when all cases are handled.

AsyncResult is serializable, which is what powers SSR/client hydration: the server renders an atom to an AsyncResult, encodes it, and the client decodes it back into the exact same state (including waiting, timestamp, and previousSuccess).

Builds a Schema.Codec for AsyncResult from optional success and error schemas. The encoded form is a tagged union of Initial / Success / Failure, with the cause encoded via Schema.Cause (covering defects too).

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Schema } from "effect"
const ResultSchema = AsyncResult.Schema({
success: Schema.Number,
error: Schema.String
})
// Encode for transport (e.g. embed in server-rendered HTML)
const encoded = Schema.encodeUnknownSync(ResultSchema)(AsyncResult.success(42))
// => { _tag: "Success", value: 42, waiting: false, timestamp: <number> }
// Decode on the client — round-trips back to a real AsyncResult
const decoded = Schema.decodeUnknownSync(ResultSchema)(encoded)
AsyncResult.isAsyncResult(decoded) // => true
AsyncResult.value(decoded) // => Option.some(42)

Both success and error are optional and default to Schema.Never — pass only what your result actually carries:

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Schema } from "effect"
// Success-only result; the error channel is never.
const OnlySuccess = AsyncResult.Schema({ success: Schema.String })

The type returned by the Schema constructor. It extends Schema.declareConstructor<...> and additionally exposes the success and error sub-schemas it was built from, so they can be reused.

import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
import { Schema } from "effect"
const ResultSchema = AsyncResult.Schema({
success: Schema.Number,
error: Schema.String
})
ResultSchema.success // => Schema.Number
ResultSchema.error // => Schema.String