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 valueAis available, along with itstimestamp.Failure— aCause<E>is available, and the latest previousSuccessmay 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"Mental model
Section titled “Mental model”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
Initialstate for “nothing has happened yet” (no value, no error) — neitherExitnorResultcan express this. waitingis an overlay, not a fourth variant. ASuccesscan bewaiting(refreshing in the background), and so can aFailure.- A
Failurecan 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 // => trueAsyncResult.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")Common case: rendering every state
Section titled “Common case: rendering every state”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.
Type and guards
Section titled “Type and guards”AsyncResult<A, E>
Section titled “AsyncResult<A, E>”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")isAsyncResult
Section titled “isAsyncResult”Returns true when an unknown value is an AsyncResult.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isAsyncResult(AsyncResult.success(1)) // => trueAsyncResult.isAsyncResult({ _tag: "Success" }) // => falseWith<R, A, E>
Section titled “With<R, A, E>”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>AsyncResult namespace
Section titled “AsyncResult namespace”The AsyncResult.AsyncResult namespace holds type-level helpers shared by every
variant:
AsyncResult.Proto<A, E>— the common prototype: pipeability, the[TypeId]marker, the phantomA/Emembers, and thewaiting: booleanflag implemented by all three variants.AsyncResult.Success<R>— extracts the success value type from anAsyncResulttype.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> // => numbertype E = AsyncResult.AsyncResult.Failure<R> // => stringState models
Section titled “State models”Initial<A, E>
Section titled “Initial<A, E>”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 // => falseSuccess<A, E>
Section titled “Success<A, E>”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 // => 42typeof ok.timestamp // => "number"Failure<A, E>
Section titled “Failure<A, E>”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) // => trueDefect / Interrupt
Section titled “Defect / Interrupt”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.
Refinements
Section titled “Refinements”isWaiting
Section titled “isWaiting”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)) // => trueAsyncResult.isWaiting(AsyncResult.success(1)) // => falseAsyncResult.isWaiting(AsyncResult.waiting(AsyncResult.success(1))) // => trueisInitial
Section titled “isInitial”Type guard narrowing to Initial<A, E>.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isInitial(AsyncResult.initial()) // => trueAsyncResult.isInitial(AsyncResult.success(1)) // => falseisNotInitial
Section titled “isNotInitial”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)) // => trueAsyncResult.isNotInitial(AsyncResult.initial()) // => falseisSuccess
Section titled “isSuccess”Type guard narrowing to Success<A, E>.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isSuccess(AsyncResult.success(1)) // => trueAsyncResult.isSuccess(AsyncResult.fail("x")) // => falseisFailure
Section titled “isFailure”Type guard narrowing to Failure<A, E>.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.isFailure(AsyncResult.fail("x")) // => trueAsyncResult.isFailure(AsyncResult.success(1)) // => falseisInterrupted
Section titled “isInterrupted”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))) // => trueAsyncResult.isInterrupted(AsyncResult.fail("boom")) // => falseConstructors
Section titled “Constructors”initial
Section titled “initial”Creates an Initial result, optionally marking it as waiting.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.initial().waiting // => falseAsyncResult.initial(true).waiting // => truesuccess
Section titled “success”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 // => trueb.timestamp // => 1000failure
Section titled “failure”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")fromExit
Section titled “fromExit”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"fromExitWithPrevious
Section titled “fromExitWithPrevious”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")failureWithPrevious
Section titled “failureWithPrevious”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)failWithPrevious
Section titled “failWithPrevious”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")waiting
Section titled “waiting”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 // => truerefreshing.value // => 42 (preserved)waitingFrom
Section titled “waitingFrom”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 // => trueCombinators
Section titled “Combinators”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 // => 42fresh.timestamp // => Date.now() (refreshed, no longer 1000)replacePrevious
Section titled “replacePrevious”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 // => 20AsyncResult.map(AsyncResult.initial<number>(), (n) => n * 10)._tag // => "Initial"
// Pipeable formAsyncResult.success(2).pipe(AsyncResult.map((n) => n + 1)).value // => 3flatMap
Section titled “flatMap”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"matchWithError
Section titled “matchWithError”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"matchWithWaiting
Section titled “matchWithWaiting”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"toExit
Section titled “toExit”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))) // => trueExit.isFailure(AsyncResult.toExit(AsyncResult.initial())) // => true (NoSuchElementError)Accessors
Section titled “Accessors”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)getOrElse
Section titled “getOrElse”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) // => 42AsyncResult.getOrElse(AsyncResult.initial<number>(), () => 0) // => 0
// Pipeable formAsyncResult.initial<number>().pipe(AsyncResult.getOrElse(() => -1)) // => -1getOrThrow
Section titled “getOrThrow”Like getOrElse but throws NoSuchElementError when no current
or previous success exists.
import { AsyncResult } from "effect/unstable/reactivity/AsyncResult"
AsyncResult.getOrThrow(AsyncResult.success(42)) // => 42AsyncResult.getOrThrow(AsyncResult.initial()) // => throws NoSuchElementErrorReturns the failure Cause as an Option — Some 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()Builder
Section titled “Builder”builder
Section titled “builder”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"Builder (type)
Section titled “Builder (type)”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 ornull.render()— produce the output, ornullfor an unhandledInitial; throws the squashed cause for an unhandledFailure.exhaustive()— produce the output, available only when all cases are handled.
Schemas
Section titled “Schemas”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).
Schema (constructor)
Section titled “Schema (constructor)”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 AsyncResultconst decoded = Schema.decodeUnknownSync(ResultSchema)(encoded)AsyncResult.isAsyncResult(decoded) // => trueAsyncResult.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 })Schema (interface)
Section titled “Schema (interface)”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.NumberResultSchema.error // => Schema.String