API reference
effect-atom-boundary
Typed error boundaries for Effect Atom: catch an atom's failures by tag, typed by the atom
import { AtomErrorBoundary, createErrorBoundary } from "effect-atom-boundary";Claim
The claiming step of typed error boundaries. A boundary claims errors by
_tag; its hook takes a read's result, throws the claimed failures to the
boundary that claims them, and returns the result typed without them.
TypeId
type TypeId = "~effect-atom-boundary/ErrorBoundary";const TypeId: TypeId;Literal type used as the boundary components' type identifier. Type identifier for the boundary components, which carry the tags they claim.
TagsOf
type TagsOf<E> = E extends {
readonly _tag: infer Tag extends string;
}
? Tag
: never;The _tags of an error union.
Unclaimed
type Unclaimed<E, Tags extends string> = Exclude<
E,
{
readonly _tag: Tags;
}
>;The errors of E whose _tag isn't in Tags.
Variance
interface Variance<Tags extends string> {
readonly [TypeId]: {
readonly _Tags: Types.Covariant<Tags>;
};
}Variance marker carrying the tags a boundary claims.
UnclaimedResult
type UnclaimedResult<R, Tags extends string> =
R extends AsyncResult.Success<infer A, infer E>
? AsyncResult.Success<A, Unclaimed<E, Tags>>
: R extends AsyncResult.Failure<infer A, infer E>
? [Unclaimed<E, Tags>] extends [never]
? never
: AsyncResult.Failure<A, Unclaimed<E, Tags>>
: R extends AsyncResult.Initial<infer A, infer E>
? AsyncResult.Initial<A, Unclaimed<E, Tags>>
: never;An AsyncResult, or a union of them, typed without the errors whose
_tag is in Tags: a failure with no error left is gone from the union,
so a read whose failures are all claimed is typed as a success.
UseErrorBoundary
interface UseErrorBoundary<Tags extends string> {
<R extends AsyncResult.AsyncResult<unknown, unknown>>(
result: R,
): UnclaimedResult<R, Tags>;
}A boundary's hook: takes the result a component read under the boundary, throws the failures this boundary and the boundaries above it claim, and returns the result typed without them.
When to use
Wrap each read under the boundary in it, such as
useIssueBoundary(useAtomResult(issue, handle)), to throw the claimed
failures to their boundaries and narrow the result to the rest.
Details
A failure whose error has a claimed _tag is thrown as the error itself,
so the nearest boundary that claims it catches it. A failure with no typed
error, a defect, is thrown too, to the error boundaries above. Any other
result is returned as it is. It takes any AsyncResult, from a read that
returns typed failures and suspends while a failed atom refreshes:
raf-effect-atom's useAtomResult, or useAtomSuspense with
includeFailure: true and suspendOnWaiting: true.
Gotchas
It's a hook, and it throws outside its boundary. It throws a claimed
failure whether or not the atom is refreshing, so a read that returns the
old failure while a reset refreshes it, such as useAtomValue, throws
it to the boundary again, and the handler stays after the refresh. A read
that throws every failure itself, such as useAtomSuspense without
includeFailure, throws before the hook sees the result.
Error boundary
Error boundaries typed by an atom's error. A boundary catches failures by
_tag, with a handler per tag typed by the error with that tag, so UI code
never imports an error class. AtomErrorBoundary guards one atom;
createErrorBoundary makes a shared boundary whose hook narrows the atoms
read under it.
CatchTagProps
interface CatchTagProps<E> {
readonly error: E;
/**
* Refreshes what failed and renders the children again: an
* `AtomErrorBoundary`'s atom, or every atom in the registry whose failure a
* `createErrorBoundary` boundary has a handler for.
*/
readonly reset: () => void;
}Props for a catchTags handler: the caught error, and reset.
CatchTags
type CatchTags<E, Tags extends TagsOf<E> = TagsOf<E>> = {
readonly [Tag in Tags]?: (
props: CatchTagProps<
Extract<
E,
{
readonly _tag: Tag;
}
>
>,
) => React.ReactNode;
};One optional handler per _tag of E (or per tag in Tags), each typed by
the error with that tag. A handler renders as a component, so it can call
hooks.
ErrorsOf
type ErrorsOf<Source> =
Source extends AtomRpc.AtomRpcClient<
infer _Self,
infer _Id,
infer _Rpcs
>
? Source["query"] extends (
...args: ReadonlyArray<never>
) => infer Query
? Query extends Atom.Atom<
AsyncResult.AsyncResult<infer _A, infer E>
>
? E
: never
: never
: Source extends AtomHttpApi.AtomHttpApiClient<
infer _Self,
infer _Id,
infer Groups
>
? EndpointErrors<HttpApiGroup.Endpoints<Groups>>
: Source extends Atom.Atom<
AsyncResult.AsyncResult<infer _A, infer E>
>
? E
: Source extends (
...args: ReadonlyArray<never>
) => Atom.Atom<AsyncResult.AsyncResult<infer _A, infer E>>
? E
: never;The error type of what a boundary's errors names: an AtomRpc client
(the errors of every procedure), an AtomHttpApi client (the errors of
every endpoint and its middleware), an atom, or an atom family.
AtomErrorBoundaryProps
interface AtomErrorBoundaryProps<A, E> {
/** The atom whose failures this boundary catches. */
readonly atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>;
/** A handler per `_tag` of the atom's error. */
readonly catchTags: NoInfer<CatchTags<E>>;
readonly children?: React.ReactNode | undefined;
}Props for an error boundary that catches one atom's failures by _tag.
AtomErrorBoundary
const AtomErrorBoundary: <A, E>(
props: AtomErrorBoundaryProps<A, E>,
) => React.ReactNode;Provides an error boundary for one atom that renders a handler per _tag
of the atom's error, typed by the error with that tag.
When to use
Use when a subtree reads one atom and some of its failures should render in place of the subtree, without the reading component handling them.
Details
The reads under it throw the atom's failures, as useAtomSuspense does
without includeFailure. A failure whose tag has a handler renders that
handler; its reset refreshes the atom and renders the children again. A
new atom clears a caught failure.
React runs no error boundaries on the server, so during server rendering
it reads atom before its children, from the registry or its hydrated
state, and renders the handler for a failure that has settled. The
browser keeps that HTML while it hydrates, then renders the children,
which throw the failure to it again.
Gotchas
A failure without a handler, or an error that isn't the one the atom fails
with in the boundary's registry, such as one a read of another atom threw,
goes to the error boundaries above. Read with suspendOnWaiting: true, so
a read waits for the refresh reset starts instead of throwing the old
failure again. reset refreshes the atom in the registry
the boundary renders in, not in a RegistryContext provided under it. A
handler renders as a component: one written inline mounts again, losing its
state, each time the parent renders.
Example (Catching a missing issue)
<AtomErrorBoundary
atom={issue(id)}
catchTags={{
IssueNotFound: ({ error, reset }) => <NotFound id={error.id} onRetry={reset} />
}}
>
<IssuePage id={id} />
</AtomErrorBoundary>
function IssuePage(props: { readonly id: number }) {
const result = useAtomSuspense(issue(props.id), { suspendOnWaiting: true })
return <h1>{result.value.title}</h1>
}See also
createErrorBoundaryfor a shared boundary that narrows the reads under it
ErrorBoundaryComponent
interface ErrorBoundaryComponent<
Tags extends string,
> extends Variance<Tags> {
(props: {
readonly children?: React.ReactNode | undefined;
}): React.ReactNode;
readonly displayName: string;
}A boundary component: renders the catchTags handler for the errors it
claims and throws the rest to the boundaries above it.
ErrorBoundary
type ErrorBoundary<Tags extends string> = readonly [
Boundary: ErrorBoundaryComponent<Tags>,
useBoundary: UseErrorBoundary<Tags>,
];A boundary component and its hook, which narrows the results read under it, named freely by destructuring.
ErrorBoundaryOptions
interface ErrorBoundaryOptions<
Source,
Tags extends TagsOf<ErrorsOf<Source>>,
ParentTags extends string,
> {
/** A handler per `_tag` this boundary claims. */
readonly catchTags: CatchTags<ErrorsOf<Source>, Tags>;
/**
* What the handlers' errors come from, for the types: an `AtomRpc` or
* `AtomHttpApi` client, an atom, or an atom family.
*/
readonly errors: Source;
/** Names it in messages and React DevTools: `"Auth"` is `<AuthBoundary>`. */
readonly name: string;
/** The boundary it sits under, which must be mounted above it. */
readonly parent?: ErrorBoundaryComponent<ParentTags> | undefined;
}Options for createErrorBoundary.
createErrorBoundary
const createErrorBoundary: <
Source,
Tags extends TagsOf<ErrorsOf<Source>>,
ParentTags extends string = never,
>(
options: ErrorBoundaryOptions<Source, Tags, ParentTags>,
) => ErrorBoundary<Tags | ParentTags>;Creates a shared error boundary that claims some errors by _tag, returned
as [Boundary, useBoundary].
When to use
Use for failures that belong to the app rather than to the component that read them, such as being signed out, so the components under the boundary only handle what's left.
Details
A component under it wraps its read in the hook:
useBoundary(useAtomResult(issue, handle)) throws the failures this
boundary or one above it claims to the nearest boundary that claims them,
and returns the result typed without them. parent is the boundary it
sits under; the claimed tags add up along the chain. catchTags' keys are tags
of errors' error type, so a misspelled tag is a type error and each
handler's error is typed.
A handler's reset refreshes every atom in the registry the boundary
renders in whose settled failure has a tag this boundary handles, not only
the one whose read threw:
one outage fails every atom on the page, and an atom that fails after the
handler replaced the children throws to no boundary.
React runs no error boundaries on the server, so during server rendering
it looks through the registry's atoms before rendering its children, and
renders its handler for a settled failure it catches. It only sees atoms
that already ran in that registry, such as the loaders' on TanStack
Start, or under raf-effect-atom's HydrationBoundary on React Router.
A failure it doesn't see is thrown on the server and caught in the
browser.
Gotchas
A boundary matches _tag only: a failure with a claimed tag from any atom
under it is caught. During server rendering, that's any atom in the
registry, read under it or not. The boundary throws without its parent
mounted above it, and the hook throws outside its boundary. The hook needs
a read that returns its failures, such as raf-effect-atom's
useAtomResult: useAtomSuspense without includeFailure throws every
failure before the hook sees it.
Example (A sign-in boundary)
const [AuthBoundary, useAuthBoundary] = createErrorBoundary({
name: "Auth",
errors: Issues,
catchTags: { Unauthorized: ({ reset }) => <SignIn onSignedIn={reset} /> }
})
function IssueTitle(props: { readonly issue: Handle<typeof issue> }) {
const result = useAuthBoundary(useAtomResult(issue, props.issue))
// AsyncResult<Issue, IssueNotFound | RpcClientError>
}See also
AtomErrorBoundaryfor a boundary that guards one atom