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

effect-atom-boundary

Error boundaries

A boundary catches the errors its catchTags name, by tag, with a handler per tag typed by the error with that tag, and the reads under it are typed without them.

effect-atom-boundary gives Effect Atom typed error boundaries. An atom's AsyncResult carries its error type, such as IssueNotFound | Unauthorized | RpcClientError for an AtomRpc query: the errors its procedure declares, and Effect RPC's RpcClientError, which every procedure fails with when the request doesn't reach the server or the answer doesn't decode. A boundary catches those failures by _tag: its catchTags keys come from the atom's error type, so UI code never imports an error class, and a misspelled tag doesn't compile.

It needs only effect, @effect/atom-react and React. It works the same with or without raf-effect-atom.

The idea comes from result-rpc, whose Shells are error boundaries that claim failures and remove them from the component's error type. effect-atom-boundary is that idea over Effect Atom, claiming by _tag.

A boundary names the errors it catches

createErrorBoundary makes a boundary from its handlers, one per _tag it catches: a tag with a handler is caught, and a tag without one isn't. issue's error is IssueNotFound | Unauthorized | RpcClientError. Here <AppBoundary> catches RpcClientError, and <IssueBoundary>, under it, catches IssueNotFound, so the read under them is left with Unauthorized:

export const [const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>): ErrorBoundary<"RpcClientError">createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "App",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>.errors: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "RpcClientError">catchTags: {
		RpcClientError?: ((props: CatchTagProps<RpcClientError>) => React.ReactNode) | undefinedRpcClientError: ({ reset: () => voidreset }) => (
			<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
				Can't reach the server. <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
			</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
		),
	},
});

export const [const IssueBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound">IssueBoundary, const useIssueBoundary: UseErrorBoundary<"RpcClientError" | "IssueNotFound">useIssueBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound", "RpcClientError">(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound", "RpcClientError">): ErrorBoundary<"RpcClientError" | "IssueNotFound">createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "Issue",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound", "RpcClientError">.errors: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound", "RpcClientError">.parent?: ErrorBoundaryComponent<"RpcClientError"> | undefinedparent: const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound", "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "IssueNotFound">catchTags: {
		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror, reset: () => voidreset }) => (
			<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
				There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}. <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
			</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
		),
	},
});

export function 
function IssuePanel(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePanel
(
props: {
    issue: Handle<typeof issue>;
}
props
: { issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) {
return ( <const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> <const IssueBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound">IssueBoundary> <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function IssueTitle(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssueTitle
issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue={
props: {
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </const IssueBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound">IssueBoundary> </const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> ); } function
function IssueTitle(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssueTitle
(
props: {
    issue: Handle<typeof issue>;
}
props
: { issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) {
const const result: AsyncResult.Success<Issue, Unauthorized> | AsyncResult.Failure<Issue, Unauthorized>result =
const useIssueBoundary: UseErrorBoundary
<SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>) => AsyncResult.Success<Issue, Unauthorized> | AsyncResult.Failure<Issue, Unauthorized>
useIssueBoundary
(useAtomResult<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(id: number & Brand<"IssueId">) => Atom<AsyncResult.AsyncResult<Issue, unknown>>>>): SettledResult<...>useAtomResult(const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
props: {
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue));
if (import AsyncResultAsyncResult.const isSuccess: <Issue, Unauthorized>(result: AsyncResult.AsyncResult<Issue, Unauthorized>) => result is AsyncResult.Success<Issue, Unauthorized>isSuccess(const result: AsyncResult.Success<Issue, Unauthorized> | AsyncResult.Failure<Issue, Unauthorized>result)) { return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const result: AsyncResult.Success<Issue, Unauthorized>result.Success<Issue, Unauthorized>.value: Issuevalue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; } const error = import OptionOption.const getOrThrow: <Unauthorized>(self: Option.Option<Unauthorized>) => UnauthorizedgetOrThrow(import AsyncResultAsyncResult.const error: <Issue, Unauthorized>(self: AsyncResult.AsyncResult<Issue, Unauthorized>) => Option.Option<Unauthorized>error(const result: AsyncResult.Failure<Issue, Unauthorized>result));
const error: Unauthorized
return import MatchMatch.
const valueTags: <Unauthorized, {
    Unauthorized: () => React.JSX.Element;
}>(input: Unauthorized, fields: {
    Unauthorized: () => React.JSX.Element;
}) => React.JSX.Element (+1 overload)
valueTags
(const error: Unauthorizederror, {
type Unauthorized: () => React.JSX.ElementUnauthorized: () => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Sign in to see this issue.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>, }); }
  • catchTags is what the boundary catches. Its keys are tags of errors' error type.
  • errors names what the errors come from, for the types only: here the issue atom family. It can also be an atom, or an AtomRpc client for the errors of all its procedures.
  • name names the boundary in messages and React DevTools: "Issue" is <IssueBoundary>.
  • parent puts <IssueBoundary> under <AppBoundary>, and the tags they catch add up. <IssueBoundary> throws when its parent isn't mounted above it.

It returns the boundary component and its hook, named by destructuring.

Reads under it are typed without what it catches

A component under <IssueBoundary> passes what it reads to useIssueBoundary. raf-effect-atom's useAtomResult returns the typed failures, and the hook throws IssueNotFound to <IssueBoundary> and RpcClientError to <AppBoundary>, and returns any other failure, typed without the caught tags. The ^? above shows Unauthorized, the one tag of issue's error that neither boundary's catchTags names, and the component handles it by its tag.

A caught failure renders the boundary's handler in place of its children. reset refreshes every atom in the registry that failed with a tag the boundary handles, and renders the children again.

The handler stays until reset: with its children replaced, the boundary can't tell that they'd now read another atom. Give a boundary around one issue's page the issue's id as its key, so opening another issue mounts a new boundary, without the last issue's failure.

A handler renders as a component, so it can call hooks. A handler written inline is a new component each time the component around the boundary renders, so it mounts again and loses its state: give a handler with state a name outside the render.

When every tag is caught

When the boundaries' catchTags name every tag of the atom's error, no failure is left. <IssuePageBoundary> catches IssueNotFound and Unauthorized, and its parent, <AppBoundary>, catches RpcClientError, so no error is left in the result's type and the hook returns a success:

export const [const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>): ErrorBoundary<"RpcClientError">createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "App",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>.errors: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "RpcClientError", never>.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "RpcClientError">catchTags: {
		RpcClientError?: ((props: CatchTagProps<RpcClientError>) => React.ReactNode) | undefinedRpcClientError: ({ reset: () => voidreset }) => (
			<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
				Can't reach the server. <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
			</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
		),
	},
});

export const [const IssuePageBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound" | "Unauthorized">IssuePageBoundary, const useIssuePageBoundary: UseErrorBoundary<"RpcClientError" | "IssueNotFound" | "Unauthorized">useIssuePageBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", "RpcClientError">(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", "RpcClientError">): ErrorBoundary<...>createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "IssuePage",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", "RpcClientError">.errors: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", "RpcClientError">.parent?: ErrorBoundaryComponent<"RpcClientError"> | undefinedparent: const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "IssueNotFound" | "Unauthorized">catchTags: {
		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror }) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
		Unauthorized?: ((props: CatchTagProps<Unauthorized>) => React.ReactNode) | undefinedUnauthorized: ({ reset: () => voidreset }) => (
			<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
				You're signed out. <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
			</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>
		),
	},
});

export function 
function IssuePage(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssuePage
(
props: {
    issue: Handle<typeof issue>;
}
props
: { issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) {
return ( <const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> <const IssuePageBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound" | "Unauthorized">IssuePageBoundary> <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function IssueTitle(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssueTitle
issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue={
props: {
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </const IssuePageBoundary: ErrorBoundaryComponent<"RpcClientError" | "IssueNotFound" | "Unauthorized">IssuePageBoundary> </const AppBoundary: ErrorBoundaryComponent<"RpcClientError">AppBoundary> ); } function
function IssueTitle(props: {
    issue: Handle<typeof issue>;
}): React.JSX.Element
IssueTitle
(
props: {
    issue: Handle<typeof issue>;
}
props
: { issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: interface Handle<S>Handle<typeof const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue> }) {
const result =
const useIssuePageBoundary: UseErrorBoundary
<SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>) => Success<Issue, never>
useIssuePageBoundary
(useAtomResult<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(id: number & Brand<"IssueId">) => Atom<AsyncResult<Issue, unknown>>>>): SettledResult<Issue, RpcClientError | ... 1 more ... | Unauthorized>useAtomResult(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
props: {
    issue: Handle<typeof issue>;
}
props
.issue: Handle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue));
const result: Success<Issue, never>
return <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const result: Success<Issue, never>result.Success<Issue, never>.value: Issuevalue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>; }

The never comes from those catchTags: without the Unauthorized handler, the result's error type is Unauthorized again, and the component has to handle it.

Handlers are typed by their tag

Each handler's error is the member of the error type with that tag:

export const [const IssueAccessBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized">IssueAccessBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", never>(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", never>): ErrorBoundary<...>createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "IssueAccess",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", never>.errors: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized", never>.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "IssueNotFound" | "Unauthorized">catchTags: {
		IssueNotFound?: ((props: CatchTagProps<IssueNotFound>) => React.ReactNode) | undefinedIssueNotFound: ({ error: IssueNotFounderror }) => {
			error;
error: IssueNotFound
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>There's no issue #{error: IssueNotFounderror.id: number & Brand<"IssueId">id}.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; }, Unauthorized?: ((props: CatchTagProps<Unauthorized>) => React.ReactNode) | undefinedUnauthorized: ({ error: Unauthorizederror }) => { error;
error: Unauthorized
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Sign in to see this issue.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; }, }, });

A tag the atom can't fail with is a type error:

export const [const NotFoundBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">NotFoundBoundary] = createErrorBoundary<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized" | "RpcClientError", never>(options: ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | "Unauthorized" | "RpcClientError", never>): ErrorBoundary<...>createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "NotFound",
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | ... 1 more ... | "RpcClientError", never>.errors: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>errors: const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
	ErrorBoundaryOptions<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, "IssueNotFound" | ... 1 more ... | "RpcClientError", never>.catchTags: CatchTags<RpcClientError | IssueNotFound | Unauthorized, "IssueNotFound" | "Unauthorized" | "RpcClientError">catchTags: {
		IssueNotFund: () => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Not found</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>,
Object literal may only specify known properties, but 'IssueNotFund' does not exist in type 'CatchTags<RpcClientError | IssueNotFound | Unauthorized, "RpcClientError" | "IssueNotFound" | "Unauthorized">'. Did you mean to write 'IssueNotFound'?
}, });

What a boundary doesn't catch

  • A failure whose tag has no handler goes to the error boundaries above it.
  • A defect or an interruption isn't a typed error: the read throws Cause.squash(cause) to the app's own error boundary.

A boundary from createErrorBoundary matches by tag: it catches its tags from any atom read under it, not only from the results passed to its hook. Shared boundaries covers boundaries for failures that belong to the app, such as RpcClientError and being signed out, and chaining them with parent.