Skip to content

raf-effect-atom and effect-atom-boundary are on npm. Both are pre-1.0, so a minor release can still break them. Install them

raf-effect-atom

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

typetype IDssince 0.1.0Source

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

typeutility typessince 0.1.0Source

type TagsOf<E> = E extends {
  readonly _tag: infer Tag extends string;
}
  ? Tag
  : never;

The _tags of an error union.

Unclaimed

typeutility typessince 0.1.0Source

type Unclaimed<E, Tags extends string> = Exclude<
  E,
  {
    readonly _tag: Tags;
  }
>;

The errors of E whose _tag isn't in Tags.

Variance

interfacemodelssince 0.1.0Source

interface Variance<Tags extends string> {
  readonly [TypeId]: {
    readonly _Tags: Types.Covariant<Tags>;
  };
}

Variance marker carrying the tags a boundary claims.

UnclaimedResult

typeutility typessince 0.3.0Source

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

interfacemodelssince 0.1.0Source

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

interfacemodelssince 0.1.0Source

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

typemodelssince 0.1.0Source

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

typeutility typessince 0.1.0Source

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

interfacecomponentssince 0.1.0Source

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

constcomponentssince 0.1.0Source

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

ErrorBoundaryComponent

interfacecomponentssince 0.1.0Source

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

typemodelssince 0.1.0Source

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

interfacemodelssince 0.1.0Source

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

constconstructorssince 0.1.0Source

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