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>:
// 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>,
},
});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)); 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.
AtomErrorBoundaryreads 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'sHydrationBoundary, 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)); // 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.