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

Together

Loader failures

A typed failure a loader hit on the server crosses to the browser as the same tagged error, where a boundary catches it or the loader turns it into a redirect.

ensure and fetch don't reject on a typed failure: the handle they return reads as the failed result. A failure from the server reaches the browser as the same tagged error, decoded by the procedure's Schema, so there are two places to handle it.

In a boundary

Let the route render and the read throw the failure to the boundary that claims it. The issue route passes its read to useIssueBoundary, so IssueNotFound goes to <IssueBoundary>, Unauthorized to <AuthBoundary> and RpcClientError to <AppBoundary>:

src/boundaries.tsx
// Under the Auth boundary: it claims IssueNotFound, and Unauthorized and
// RpcClientError still go up the chain.
export const [const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary, const useIssueBoundary: UseErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">useIssueBoundary] = createErrorBoundary<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">(options: ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">): ErrorBoundary<"IssueNotFound" | "Unauthorized" | "RpcClientError">createErrorBoundary({
	ErrorBoundaryOptions<Source, Tags extends TagsOf<ErrorsOf<Source>>, ParentTags extends string>.name: stringname: "Issue",
	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.errors: typeof IssuesClienterrors: class IssuesClientIssuesClient,
	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.parent?: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError"> | undefinedparent: const AuthBoundary: ErrorBoundaryComponent<"Unauthorized" | "RpcClientError">AuthBoundary,
	ErrorBoundaryOptions<typeof IssuesClient, "IssueNotFound", "Unauthorized" | "RpcClientError">.catchTags: CatchTags<RpcClientError | IssueNotFound | EmptyTitle | EmptyBody | EmptyComment | Unauthorized, "IssueNotFound">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>,
	},
});
src/routes/issues.$id.tsx
export const 
const Route: Route<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
    readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$id")({
params?: {
    parse?: ParseParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
    priority?: number;
    stringify?: StringifyParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
} | undefined
params
: {
parse?: ParseParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
parse
: (
params: {
    id: string;
}
params
) => import SchemaSchema.
function decodeUnknownSync<Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
    readonly id: number & Brand<"IssueId">;
}
decodeUnknownSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
)(
params: {
    id: string;
}
params
),
stringify?: StringifyParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
stringify
: import SchemaSchema.
const encodeSync: <Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions) => (input: {
    readonly id: number & Brand<"IssueId">;
}, options?: ParseOptions) => {
    readonly id: string;
}
encodeSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
),
},
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader; }, AnyContext, AnyContext, {}, undefined, ... 4 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async ({
context: {
    atoms: Loader;
}
context
,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
}) => ({
// Starts the assignee; it streams in while the page renders. assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:
context: {
    atoms: Loader;
}
context
.atoms: Loaderatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id),
// Waits for the issue, so the page renders with it. issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: await
context: {
    atoms: Loader;
}
context
.atoms: Loaderatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id),
}), UpdatableRouteOptionsExtensions.component?: RouteComponent | undefinedcomponent: () => ( <const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> <function IssuePage(): React.JSX.ElementIssuePage /> </const IssueBoundary: ErrorBoundaryComponent<"IssueNotFound" | "Unauthorized" | "RpcClientError">IssueBoundary> ), }); function function IssuePage(): React.JSX.ElementIssuePage() { const
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
=
const Route: Route<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
    readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
.
RouteExtensions<"/issues/$id", "/issues/$id">.useLoaderData: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    atoms: Loader;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown, boolean>(opts?: (UseLoaderDataBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    atoms: Loader;
}, AnyContext, ... 7 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/issues/$id", true, unknown, boolean> & OptionalStructuralSharing<...>) | undefined) => {
    ...;
}
useLoaderData
();
// IssueNotFound, Unauthorized and RpcClientError go to their boundaries, // so no failure is left. const result = useIssueBoundary<SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>): Success<Issue, never>useIssueBoundary(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,
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
.issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue));
const result: Success<Issue, never>
return ( <React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> <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> <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading the assignee…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function Assignee(props: {
    assignee: Handle<typeof assignee>;
}): React.JSX.Element
Assignee
assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee={
const data: {
    assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>;
    issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>;
}
data
.assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> </React.JSX.IntrinsicElements.article: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>article> ); } // Takes the handle the loader returned, not an id: it reads a request that // already started, and can't start its own. function
function Assignee(props: {
    assignee: Handle<typeof assignee>;
}): React.JSX.Element
Assignee
(
props: {
    assignee: Handle<typeof assignee>;
}
props
: { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee: interface Handle<S>Handle<typeof const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee> }) {
const const result: Success<User, never>result = useIssueBoundary<SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>>(result: SettledResult<User, RpcClientError | IssueNotFound | Unauthorized>): Success<User, never>useIssueBoundary(useAtomResult<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, handle: NoInfer<Handle<(issueId: number & Brand<"IssueId">) => Atom<AsyncResult<User, unknown>>>>): SettledResult<...>useAtomResult(const assignee: (issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
props: {
    assignee: Handle<typeof assignee>;
}
props
.assignee: Handle<(issueId: IssueId) => Atom<AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee));
return <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{const result: Success<User, never>result.Success<User, never>.value: Uservalue.name: stringname}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>; }

The server renders the boundary's handler and sends the failed result with the page. The browser keeps the handler's HTML while it hydrates, then the read throws the hydrated failure and the boundary catches it, all without a request. reset refreshes the atom in the browser.

During server rendering

React runs no error boundaries on the server: a read that throws there fails the response, or leaves that part of the page to the browser. So a boundary checks the registry before it renders its children, and renders its handler in their place for a failure it catches that has settled.

  • AtomErrorBoundary reads its own atom, from the registry or from the state the page hydrated, under every framework.
  • A shared boundary looks through the atoms that already ran in the registry it reads: the loaders' on TanStack Start, and on React Router, the request's under HydrationBoundary. A shared boundary above a route's HydrationBoundary, or in a Next.js client component, doesn't see them.
  • A failure that settles after the boundary rendered, such as one a loader prefetched, streams to the browser instead.

A failure the boundary doesn't see is thrown on the server, and the browser renders the handler. On React Router, a throw before the page's shell is ready makes the response a 500.

In the loader

When a failure should be the router's not-found page or a redirect, the loader reads the result's error:

export const 
const Route: Route<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
    readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & FilebaseRouteOptionsInterface<...> & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$id")({
params?: {
    parse?: ParseParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
    priority?: number;
    stringify?: StringifyParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
} | undefined
params
: {
parse?: ParseParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
parse
: (
params: {
    id: string;
}
params
) => import SchemaSchema.
function decodeUnknownSync<Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
    readonly id: number & Brand<"IssueId">;
}
decodeUnknownSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
)(
params: {
    id: string;
}
params
),
stringify?: StringifyParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
stringify
: import SchemaSchema.
const encodeSync: <Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions) => (input: {
    readonly id: number & Brand<"IssueId">;
}, options?: ParseOptions) => {
    readonly id: string;
}
encodeSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
),
},
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async ({
context: {
    atoms: Loader.Loader;
}
context
,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
}) => {
const const handle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>handle = await
context: {
    atoms: Loader.Loader;
}
context
.atoms: Loader.Loaderatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id);
const error = import AsyncResultAsyncResult.const error: <Issue, RpcClientError | IssueNotFound | Unauthorized>(self: AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>) => Option.Option<RpcClientError | IssueNotFound | Unauthorized>error(import LoaderLoader.const result: <(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(handle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>) => Loader.SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>result(const handle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>handle));
const error: Option.Option<RpcClientError | IssueNotFound | Unauthorized>
// Only IssueNotFound becomes a 404. Unauthorized and RpcClientError // stay in the result, for the page's boundaries. if ( import OptionOption.const isSome: <RpcClientError | IssueNotFound | Unauthorized>(self: Option.Option<RpcClientError | IssueNotFound | Unauthorized>) => self is Option.Some<RpcClientError | IssueNotFound | Unauthorized>isSome(const error: Option.Option<RpcClientError | IssueNotFound | Unauthorized>error) && import PredicatePredicate.
const isTagged: <"IssueNotFound">(self: unknown, tag: "IssueNotFound") => self is {
    _tag: "IssueNotFound";
} (+1 overload)
isTagged
(const error: Option.Some<RpcClientError | IssueNotFound | Unauthorized>error.Some<RpcClientError | IssueNotFound | Unauthorized>.value: RpcClientError | IssueNotFound | Unauthorizedvalue, "IssueNotFound")
) { throw function notFound(options?: NotFoundError): NotFoundErrornotFound(); } return { issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: const handle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>handle }; }, });

The typed failure stays a value until the loader decides what it means. Only a defect or an interruption rejects, and goes to the router's error component.

Reads behind a check

The server sends every atom the loaders started, so data the page should carry only once a check passes, such as an access check, starts after the check rather than with it. The loader ensures the check and starts the reads it guards only when it succeeded:

export const 
const GuardedRoute: Route<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id", "/issues/$id", undefined, {
    readonly id: number & Brand<"IssueId">;
}, AnyContext, AnyContext, AnyContext, {}, ({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
GuardedRoute
=
createFileRoute<"/issues/$id", RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$id", "/issues/$id", "/issues/$id">(path?: "/issues/$id" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & FilebaseRouteOptionsInterface<...> & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$id")({
params?: {
    parse?: ParseParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
    priority?: number;
    stringify?: StringifyParamsFn<"/issues/$id", {
        readonly id: number & Brand<"IssueId">;
    }> | undefined;
} | undefined
params
: {
parse?: ParseParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
parse
: (
params: {
    id: string;
}
params
) => import SchemaSchema.
function decodeUnknownSync<Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions): (input: unknown, options?: ParseOptions) => {
    readonly id: number & Brand<"IssueId">;
}
decodeUnknownSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
)(
params: {
    id: string;
}
params
),
stringify?: StringifyParamsFn<"/issues/$id", {
    readonly id: number & Brand<"IssueId">;
}> | undefined
stringify
: import SchemaSchema.
const encodeSync: <Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>>(schema: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>, options?: ParseOptions) => (input: {
    readonly id: number & Brand<"IssueId">;
}, options?: ParseOptions) => {
    readonly id: string;
}
encodeSync
(
const IssueParams: Schema.Struct<{
    readonly id: Schema.compose<Schema.brand<Schema.Int, "IssueId">, Schema.NumberFromString>;
}>
IssueParams
),
},
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { atoms: Loader.Loader; }, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, ... 12 more ..., undefined>.loader?: Constrain<({ context, params }: LoaderFnContext<Register, RootRoute<Register, undefined, {
    atoms: Loader.Loader;
}, AnyContext, AnyContext, ... 6 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefined
loader
: async ({
context: {
    atoms: Loader.Loader;
}
context
,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
}) => {
// The check: GetIssue fails with Unauthorized for a signed-out user. const const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle = await
context: {
    atoms: Loader.Loader;
}
context
.atoms: Loader.Loaderatoms.Loader.ensure: <(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(source: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>, id: number & Brand<"IssueId">) => Promise<LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>>ensure(const issue: (id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>issue,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id);
if (import AsyncResultAsyncResult.const isFailure: <Issue, RpcClientError | IssueNotFound | Unauthorized>(result: AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>) => result is AsyncResult.Failure<Issue, RpcClientError | IssueNotFound | Unauthorized>isFailure(import LoaderLoader.const result: <(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>(handle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>) => Loader.SettledResult<Issue, RpcClientError | IssueNotFound | Unauthorized>result(const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle))) { // The assignee never starts, so the page carries none of it. return { assignee: undefinedassignee: var undefinedundefined, issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle }; } return { assignee: Handle<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>assignee:
context: {
    atoms: Loader.Loader;
}
context
.atoms: Loader.Loaderatoms.Loader.prefetch: <(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>(source: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>, issueId: number & Brand<"IssueId">) => Handle<(issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>>prefetch(const assignee: (issueId: IssueId) => Atom<AsyncResult.AsyncResult<User, RpcClientError | IssueNotFound | Unauthorized>>assignee,
params: {
    readonly id: number & Brand<"IssueId">;
}
params
.id: number & Brand<"IssueId">id),
issue: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issue: const issueHandle: LoadedHandle<(id: IssueId) => Atom<AsyncResult.AsyncResult<Issue, RpcClientError | IssueNotFound | Unauthorized>>>issueHandle, }; }, });

A failed check sends none of the guarded atoms, even ones that would have succeeded. The reads wait for the check, one round trip, which on the server is usually an in-process call, and still run alongside each other. The server should still authorize each read on its own: the check decides what the page shows, not what a request may fetch. Once every endpoint does, the loader can start the reads with the check instead of after it: see A shell that gates the reads inside it.