Skip to content

raf-query is early. The API can still change between minor releases, so use it at your own risk.

raf-query

Integrations

Effect typed errors

An Effect queryFn's error type is the query's. useQuery returns a Result so you match failures inline; catchTag in loaders and actions; or throwOnError to an error boundary.

An Effect queryFn's error type is the query's error type. It flows through the factory: ErrorOf<TFactory> names it, and every verb on the cache fails with it. Catch the failures you expect with Effect.catchTag, and the type tells you what's left.

In React, a client with a layer defaults to throwOnError: false: useQuery gives you a Result<Data, ErrorOf> so you match failures inline with Effect's Result. Promise queryFns keep a rejecting promise — their ErrorOf is unknown, so a Result would not help.

The examples use the issue tracker from Using Effect: an IssueApi service whose methods fail with IssueNotFound, Unauthorized and EmptyTitle.

Inferred from the queryFn

export function 
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
(
input: {
    id: number;
}
input
: { id: numberid: number }) {
return
queryOptions<Issue, IssueNotFound | Unauthorized, IssueApi, readonly ["issue", number], undefined>(options: Omit<QueryOptions<{
    data: Issue;
    error: IssueNotFound | Unauthorized;
    key: readonly ["issue", number];
    services: IssueApi;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
queryOptions
({
queryKey: readonly ["issue", number]queryKey: ["issue",
input: {
    id: number;
}
input
.id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
    data: Issue;
    error: IssueNotFound | Unauthorized;
    key: readonly ["issue", number];
    services: IssueApi;
}>
queryFn
: () => class IssueApiIssueApi.
Service<IssueApi, { readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>; readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>; readonly rename: (input: { readonly id: number; readonly title: string; }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>; }>.use<Issue, IssueNotFound | Unauthorized, never>(f: (service: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}) => Effect.Effect<...>): Effect.Effect<...>
use
((
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
) =>
api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
.get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>get(
input: {
    id: number;
}
input
.id: numberid)),
}); } type IssueError = type ErrorOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TTypes extends InfiniteQueryTypes> ? ErrorIn<TTypes> : ReturnType<TFactory> extends QueryOptions<infer TTypes extends QueryTypes> ? ErrorIn<TTypes> : unknownErrorOf<typeof
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
>;
type IssueError = IssueNotFound | Unauthorized
const
const issue: Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, IssueNotFound | Unauthorized | CancelledError, IssueApi | QueryCache.QueryCache>
issue
= import EffectEffect.
const gen: <Effect.Effect<QueryCache.QueryCache, never, QueryCache.QueryCache> | Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, IssueNotFound | Unauthorized | CancelledError, IssueApi>, QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>>(f: () => Generator<...>) => Effect.Effect<...> (+1 overload)
gen
(function* () {
const const cache: QueryCache.QueryCachecache = yield* import QueryCacheQueryCache.const QueryCache: Service<QueryCache.QueryCache, QueryCache.QueryCache>QueryCache; return yield* const cache: QueryCache.QueryCachecache.
QueryCache.fetch<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, input: {
    id: number;
}): Loaded<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>> (+1 overload)
fetch
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
, { id: numberid: 1 });
}); issue;
const issue: Effect.Effect<QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, IssueNotFound | Unauthorized | CancelledError, IssueApi | QueryCache.QueryCache>

ensure and fetch add Query.CancelledError, for a request that was cancelled while they waited. prefetch never fails: it returns a handle, and readers of the handle see the error.

In React

With a layer, the client defaults to throwOnError: false. useQuery then returns a thenable of Result<Data, ErrorOf> — use() suspends while pending and never throws. Match the Result:

function 
function Assignee(props: {
    handle: QueryHandle<typeof assigneeQuery>;
}): React.JSX.Element
Assignee
(
props: {
    handle: QueryHandle<typeof assigneeQuery>;
}
props
: {
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle<typeof
function assigneeQuery(input: {
    issueId: number;
}): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
assigneeQuery
> }) {
const [const assigneePromise: Thenable<Result.Result<User, IssueNotFound | Unauthorized>>assigneePromise,
const controls: QueryControls<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
controls
] =
useQuery<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, undefined>(factory: (input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, options?: (Options & ... 1 more ... & {
    ...;
}) | undefined): readonly [promise: ...] (+1 overload)
useQuery
(
function assigneeQuery(input: {
    issueId: number;
}): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
assigneeQuery
,
props: {
    handle: QueryHandle<typeof assigneeQuery>;
}
props
.
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
);
const assignee = use<Result.Result<User, IssueNotFound | Unauthorized>>(usable: React.Usable<Result.Result<User, IssueNotFound | Unauthorized>>): Result.Result<User, IssueNotFound | Unauthorized>use(const assigneePromise: Thenable<Result.Result<User, IssueNotFound | Unauthorized>>assigneePromise);
const assignee: Result.Result<User, IssueNotFound | Unauthorized>
return import ResultResult.
const match: <User, IssueNotFound | Unauthorized, React.JSX.Element, React.JSX.Element>(self: Result.Result<User, IssueNotFound | Unauthorized>, options: {
    readonly onFailure: (error: IssueNotFound | Unauthorized) => React.JSX.Element;
    readonly onSuccess: (ok: User) => React.JSX.Element;
}) => React.JSX.Element (+1 overload)
match
(const assignee: Result.Result<User, IssueNotFound | Unauthorized>assignee, {
onFailure: (error: IssueNotFound | Unauthorized) => React.JSX.ElementonFailure: (error: IssueNotFound | Unauthorizederror) => error: IssueNotFound | Unauthorizederror._tag: "IssueNotFound" | "Unauthorized"_tag === "IssueNotFound" ? ( <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Unassigned</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> ) : ( <React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.role?: React.AriaRole | undefinedrole="alert"> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Couldn't load the assignee.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => void
const controls: QueryControls<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
controls
.QueryControls<(input: { issueId: number; }) => QueryOptions<Flat<{ data: User; key: readonly ["issue", number, "assignee"]; } & { error: IssueNotFound | Unauthorized; } & { ...; }>>>.refetch(): Promise<void>refetch()}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>
</React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ), onSuccess: (ok: User) => React.JSX.ElementonSuccess: (user: Useruser) => <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>{user: Useruser.User.name: stringname}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>, }); } function
function AssigneeSection(props: {
    handle: QueryHandle<typeof assigneeQuery>;
}): React.JSX.Element
AssigneeSection
(
props: {
    handle: QueryHandle<typeof assigneeQuery>;
}
props
: {
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
: interface QueryHandle<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandle<typeof
function assigneeQuery(input: {
    issueId: number;
}): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
assigneeQuery
> }) {
return ( <const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense React.SuspenseProps.fallback?: React.ReactNodefallback={<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Loading assignee…</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>}> <
function Assignee(props: {
    handle: QueryHandle<typeof assigneeQuery>;
}): React.JSX.Element
Assignee
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
={
props: {
    handle: QueryHandle<typeof assigneeQuery>;
}
props
.
handle: QueryHandle<(input: {
    issueId: number;
}) => QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
} />
</const Suspense: React.ExoticComponent<React.SuspenseProps>Suspense> ); }

Set throwOnError: true on the query, on the call, or on the client to get a rejecting promise again (and throw to an error boundary). The call's option only exists in the type when ErrorOf is typed — promise queryFns always throw. On the same client, a query whose queryFn returns a promise still reads as plain data: useQuery tells the two apart by what the queryFn returned, on this client or on the server whose data was hydrated, so Effect and promise queries mix freely.

Set it on the query

A query whose failures always belong to an error boundary sets throwOnError in its options, so no read has to repeat it. Reads that don't set their own follow it, and so do their types: TypeScript can't see the flag a client was built with, but it sees the factory's. A read's own throwOnError still wins:

function 
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>
issueQuery
(
input: {
    id: number;
}
input
: { id: numberid: number }) {
return
queryOptions<{
    title: string;
}, IssueNotFound, never, readonly ["issue", number], true>(options: Omit<QueryOptions<{
    data: {
        title: string;
    };
    error: IssueNotFound;
    key: readonly ["issue", number];
    services: never;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<true>): QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>
queryOptions
({
queryKey: readonly ["issue", number]queryKey: ["issue",
input: {
    id: number;
}
input
.id: numberid],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
    data: {
        title: string;
    };
    error: IssueNotFound;
    key: readonly ["issue", number];
    services: never;
}>
queryFn
: () =>
function getIssue(id: number): Effect.Effect<{
    title: string;
}, IssueNotFound>
getIssue
(
input: {
    id: number;
}
input
.id: numberid),
ThrowOnErrorField<true>.throwOnError?: true | undefinedthrowOnError: true, }); } const [issuePromise] =
useQuery<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>, undefined>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>, handle: QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>>, options?: (Options & ... 1 more ... & {
    ...;
}) | undefined): readonly [promise: ...] (+1 overload)
useQuery
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>
issueQuery
,
const handle: QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>>
handle
);
const issuePromise: Thenable<{
    title: string;
}>
const [issueResult] =
useQuery<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>, false>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>, handle: QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>>, options?: (Options & ... 1 more ... & {
    ...;
}) | undefined): readonly [promise: ...] (+1 overload)
useQuery
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>
issueQuery
,
const handle: QueryHandle<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: {
        title: string;
    };
    key: readonly ["issue", number];
} & {
    error: IssueNotFound;
} & {
    throwOnError: true;
}>>>
handle
, { throwOnError?: false | undefinedthrowOnError: false });
const issueResult: Thenable<Result<{
    title: string;
}, IssueNotFound>>

A read takes its flag from the call, else the query, else the client. Set it on the query when every read of it should agree, for example in a factory maker that builds a whole API's queries. If you pass queryOptions' type parameters yourself, pass the fifth, TThrow, as well (queryOptions<Data, Error, Services, QueryKey, true>), or set the flag through inference.

retry and retryDelay still get the error as unknown (query defaults share them). Narrow with the schema there too:

export function 
function assigneeQuery(input: {
    issueId: number;
}): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
assigneeQuery
(
input: {
    issueId: number;
}
input
: { issueId: numberissueId: number }) {
return
queryOptions<User, IssueNotFound | Unauthorized, IssueApi, readonly ["issue", number, "assignee"], undefined>(options: Omit<QueryOptions<{
    data: User;
    error: IssueNotFound | Unauthorized;
    key: readonly ["issue", number, "assignee"];
    services: IssueApi;
}>, "kind" | "throwOnError"> & ThrowOnErrorField<undefined>): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    ...;
}>>
queryOptions
({
queryKey: readonly ["issue", number, "assignee"]queryKey: ["issue",
input: {
    issueId: number;
}
input
.issueId: numberissueId, "assignee"],
function queryFn(context: QueryFunctionContext): QueryFnResult<{
    data: User;
    error: IssueNotFound | Unauthorized;
    key: readonly ["issue", number, "assignee"];
    services: IssueApi;
}>
queryFn
: () => class IssueApiIssueApi.
Service<IssueApi, { readonly get: (id: number) => Effect<Issue, IssueNotFound | Unauthorized>; readonly assignee: (issueId: number) => Effect<User, IssueNotFound | Unauthorized>; readonly rename: (input: { readonly id: number; readonly title: string; }) => Effect<Issue, EmptyTitle | Unauthorized>; }>.use<User, IssueNotFound | Unauthorized, never>(f: (service: {
    readonly get: (id: number) => Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect<Issue, EmptyTitle | Unauthorized>;
}) => Effect<...>): Effect<...>
use
((
api: {
    readonly get: (id: number) => Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect<Issue, EmptyTitle | Unauthorized>;
}
api
) =>
api: {
    readonly get: (id: number) => Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect<Issue, EmptyTitle | Unauthorized>;
}
api
.assignee: (issueId: number) => Effect<User, IssueNotFound | Unauthorized>assignee(
input: {
    issueId: number;
}
input
.issueId: numberissueId)),
retry?: RetryPolicy | undefinedretry: (failures: numberfailures, error: unknownerror) => failures: numberfailures <= 3 && !import SchemaSchema.const is: <typeof Unauthorized>(schema: typeof Unauthorized) => <I>(input: I) => input is I & Unauthorizedis(class UnauthorizedUnauthorized)(error: unknownerror), }); } type AssigneeError = type ErrorOf<TFactory extends QueryFactory> = ReturnType<TFactory> extends InfiniteQueryOptions<infer TTypes extends InfiniteQueryTypes> ? ErrorIn<TTypes> : ReturnType<TFactory> extends QueryOptions<infer TTypes extends QueryTypes> ? ErrorIn<TTypes> : unknownErrorOf<typeof
function assigneeQuery(input: {
    issueId: number;
}): QueryOptions<Flat<{
    data: User;
    key: readonly ["issue", number, "assignee"];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
assigneeQuery
>;
type AssigneeError = IssueNotFound | Unauthorized

Catch it where you call

Loaders that only prefetch or ensure use the Promise verbs, the same as a non-Effect app. When the loader itself needs Effect — a typed catchTag, or yield* of a service — write it as an Effect.fn that yield*s the QueryCache service, and run it with queryClient.runPromise. That runs it with the services of the client's layer, as Effect's ManagedRuntime does. The failures stay typed until the Effect catches them, and the promise rejects with whatever is left.

A loader can turn a typed failure into the router's own error. Here a missing issue becomes TanStack Router's notFound(), so the route renders its not-found page, and anything else reaches errorComponent:

src/routes/issues.$issueId.tsx
const 
const loadIssue: (id: number) => Effect.Effect<{
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | CancelledError | NotFoundError, QueryCache.QueryCache | IssueApi>
loadIssue
= import EffectEffect.const fn: (name: string, options?: SpanOptionsNoTrace) => Effect.fn.Traced (+63 overloads)fn("loadIssue")(
function* (id: numberid: number) { const const cache: QueryCache.QueryCachecache = yield* import QueryCacheQueryCache.const QueryCache: Service<QueryCache.QueryCache, QueryCache.QueryCache>QueryCache; return {
issue: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
issue
: yield* const cache: QueryCache.QueryCachecache.
QueryCache.ensure<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, input: {
    id: number;
}): Loaded<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>> (+1 overload)
ensure
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
, { id: numberid }) };
}, import EffectEffect.const catchTag: <"IssueNotFound", IssueNotFound | Unauthorized | CancelledError, never, NotFoundError, never, unassigned, never, never>(k: "IssueNotFound", f: (e: IssueNotFound) => Effect.Effect<never, NotFoundError, never>, orElse?: ((e: Unauthorized | CancelledError) => Effect.Effect<unassigned, never, never>) | undefined) => <A, R>(self: Effect.Effect<A, IssueNotFound | Unauthorized | CancelledError, R>) => Effect.Effect<...> (+1 overload)catchTag("IssueNotFound", () => import EffectEffect.const fail: <NotFoundError>(error: NotFoundError) => Effect.Effect<never, NotFoundError, never>fail(function notFound(options?: NotFoundError): NotFoundErrornotFound())), ); export const
const Route: Route<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", "/issues/$issueId", undefined, ResolveParams<"/issues/$issueId">, AnyContext, AnyContext, AnyContext, {}, (args: LoaderFnContext<Register, RootRoute<Register, undefined, ... 9 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, ... 4 more ..., undefined>
Route
=
createFileRoute<"/issues/$issueId", RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", "/issues/$issueId", "/issues/$issueId">(path?: "/issues/$issueId" | undefined): <TRegister, TSearchValidator, TParams, TRouteContextFn, TBeforeLoadFn, TLoaderDeps, TLoaderFn, TChildren, TSSR, TMiddlewares, THandlers>(options?: (ParamsOptions<...> & ... 1 more ... & UpdatableRouteOptions<...>) | undefined) => Route<...>
createFileRoute
("/issues/$issueId")({
FilebaseRouteOptionsInterface<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 12 more ..., undefined>.loader?: Constrain<(args: LoaderFnContext<Register, RootRoute<Register, undefined, ... 9 more ..., undefined>, ... 7 more ..., undefined>) => Promise<...>, RouteLoaderFn<...> | RouteLoaderObject<...>> | undefinedloader: (
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
) =>
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>.context: {
    queryClient: QueryClient<IssueApi>;
}
context
.queryClient: QueryClient<IssueApi>queryClient.
QueryClient<IssueApi>.runPromise<{
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | CancelledError | NotFoundError>(effect: Effect.Effect<{
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | ... 1 more ... | NotFoundError, QueryCache.QueryCache | IssueApi>): Promise<...>
runPromise
(
const loadIssue: (id: number) => Effect.Effect<{
    issue: QueryHandleWithData<(input: {
        id: number;
    }) => QueryOptions<Flat<{
        data: Issue;
        key: readonly ["issue", number];
    } & {
        error: IssueNotFound | Unauthorized;
    } & {
        services: IssueApi;
    }>>>;
}, Unauthorized | CancelledError | NotFoundError, QueryCache.QueryCache | IssueApi>
loadIssue
(
var Number: NumberConstructor
(value?: any) => number
Number
(
args: LoaderFnContext<Register, RootRoute<Register, undefined, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, unknown, unknown, unknown, unknown, undefined>, "/issues/$issueId", ResolveParams<"/issues/$issueId">, {}, AnyContext, AnyContext, AnyContext, unknown, undefined>
args
.
LoaderFnContext<Register, RootRoute<Register, undefined, { queryClient: QueryClient<IssueApi>; }, AnyContext, ... 7 more ..., undefined>, ... 7 more ..., undefined>.params: {
    issueId: string;
}
params
.issueId: stringissueId))),
UpdatableRouteOptionsExtensions.errorComponent?: false | ErrorRouteComponent | null | undefinederrorComponent: (props: ErrorComponentPropsprops) => ( <React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.role?: React.AriaRole | undefinedrole="alert"> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>Couldn't load the issue.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={props: ErrorComponentPropsprops.reset: () => voidreset}>Try again</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> </React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div> ), });

catchTag("IssueNotFound", …) type-checks because ErrorOf<typeof issueQuery> includes that tag. Leave a tag out and TypeScript rejects the call.

A form action can catch every failure it declares into the message the form shows. Effect.catchTags lists them, and the Effect is left with never in its error channel, so its promise never rejects:

src/components/rename-form.tsx
const 
const rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<string | null, never, IssueApi | QueryCache.QueryCache>
rename
= import EffectEffect.const fn: (name: string, options?: SpanOptionsNoTrace) => Effect.fn.Traced (+63 overloads)fn("rename")(
function* (
input: {
    readonly id: number;
    readonly title: string;
}
input
: { readonly id: numberid: number; readonly title: stringtitle: string }) {
const
const api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
= yield* class IssueApiIssueApi;
const const cache: QueryCache.QueryCachecache = yield* import QueryCacheQueryCache.const QueryCache: Service<QueryCache.QueryCache, QueryCache.QueryCache>QueryCache; yield*
const api: {
    readonly get: (id: number) => Effect.Effect<Issue, IssueNotFound | Unauthorized>;
    readonly assignee: (issueId: number) => Effect.Effect<User, IssueNotFound | Unauthorized>;
    readonly rename: (input: {
        readonly id: number;
        readonly title: string;
    }) => Effect.Effect<Issue, EmptyTitle | Unauthorized>;
}
api
.
rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<Issue, EmptyTitle | Unauthorized>
rename
(
input: {
    readonly id: number;
    readonly title: string;
}
input
);
yield* const cache: QueryCache.QueryCachecache.QueryCache.invalidate<QueryFactory>(...target: QueryCache.Target<QueryFactory>): Effect.Effect<void>invalidate({ Filter.queryKey?: QueryKey | undefinedqueryKey: ["issue",
input: {
    readonly id: number;
    readonly title: string;
}
input
.id: numberid] });
return null; }, import EffectEffect.
const catchTags: <Unauthorized | EmptyTitle, {
    EmptyTitle: () => Effect.Effect<string, never, never>;
    Unauthorized: () => Effect.Effect<string, never, never>;
}, unassigned, never, never>(cases: {
    EmptyTitle: () => Effect.Effect<string, never, never>;
    Unauthorized: () => Effect.Effect<string, never, never>;
}, orElse?: ((e: never) => Effect.Effect<unassigned, never, never>) | undefined) => <A, R>(self: Effect.Effect<A, Unauthorized | EmptyTitle, R>) => Effect.Effect<...> (+1 overload)
catchTags
({
type EmptyTitle: () => Effect.Effect<string, never, never>EmptyTitle: () => import EffectEffect.const succeed: <string>(value: string) => Effect.Effect<string, never, never>succeed("Title can't be empty"), type Unauthorized: () => Effect.Effect<string, never, never>Unauthorized: () => import EffectEffect.const succeed: <string>(value: string) => Effect.Effect<string, never, never>succeed("Sign in to rename issues"), }), ); const renaming =
const rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<string | null, never, IssueApi | QueryCache.QueryCache>
rename
({ id: numberid: 1, title: stringtitle: "" });
const renaming: Effect.Effect<string | null, never, IssueApi | QueryCache.QueryCache>
export function
function RenameForm(props: {
    issue: Issue;
}): React.JSX.Element
RenameForm
(
props: {
    issue: Issue;
}
props
: { issue: Issueissue: Issue }) {
const const queryClient: QueryClient<IssueApi>queryClient =
const Route: RouteApi<"/issues/$issueId", RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>>
Route
.
RouteApi<"/issues/$issueId", RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, { queryClient: QueryClient<IssueApi>; }, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>>.useRouteContext: <RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, AnyContext, AnyContext, {}, undefined, RootRouteChildren, FileRouteTypes, unknown, unknown, undefined>, "never", false, RouterHistory, Record<string, any>>, unknown>(opts?: UseRouteContextBaseOptions<RouterCore<Route<Register, any, "/", "/", string, "__root__", undefined, {}, {
    queryClient: QueryClient<IssueApi>;
}, ... 8 more ..., undefined>, "never", false, RouterHistory, Record<...>>, "/issues/$issueId", true, unknown> | undefined) => {
    ...;
}
useRouteContext
().queryClient: QueryClient<IssueApi>queryClient;
const [const message: string | nullmessage, const action: (payload: FormData) => voidaction, const isPending: booleanisPending] = useActionState<string | null, FormData>(action: (state: string | null, payload: FormData) => string | Promise<string | null> | null, initialState: string | null, permalink?: string): [state: string | null, dispatch: (payload: FormData) => void, isPending: boolean] (+1 overload)useActionState( (_previous: string | null_previous: string | null, formData: FormDataformData: FormData) => const queryClient: QueryClient<IssueApi>queryClient.QueryClient<IssueApi>.runPromise<string | null, never>(effect: Effect.Effect<string | null, never, IssueApi | QueryCache.QueryCache>): Promise<string | null>runPromise(
const rename: (input: {
    readonly id: number;
    readonly title: string;
}) => Effect.Effect<string | null, never, IssueApi | QueryCache.QueryCache>
rename
({ id: numberid:
props: {
    issue: Issue;
}
props
.issue: Issueissue.Issue.id: numberid, title: stringtitle:
var String: StringConstructor
(value?: any) => string
String
(formData: FormDataformData.FormData.get(name: string): FormDataEntryValue | nullget("title")) }),
), null, ); return ( <React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form React.FormHTMLAttributes<HTMLFormElement>.action?: string | ((formData: FormData) => void | Promise<void>) | undefinedaction={const action: (payload: FormData) => voidaction}> <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>input React.HTMLAttributes<HTMLInputElement>.defaultValue?: string | number | readonly string[] | undefineddefaultValue={
props: {
    issue: Issue;
}
props
.issue: Issueissue.Issue.title: stringtitle} React.InputHTMLAttributes<HTMLInputElement>.name?: string | undefinedname="title" />
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.ButtonHTMLAttributes<HTMLButtonElement>.disabled?: boolean | undefineddisabled={const isPending: booleanisPending}>Rename</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> {const message: string | nullmessage === null ? null : <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p React.HTMLAttributes<HTMLParagraphElement>.role?: React.AriaRole | undefinedrole="alert">{const message: stringmessage}</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p>} </React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>form> ); }

Leave a tag out of catchTags and it stays in the error type: the promise can reject with it, and the action throws it to the nearest error boundary.

Or let it throw

You don't have to match a Result. Set throwOnError: true on the query, the call or the client, and a failure the Effect leaves uncaught behaves like a rejected promise:

  • The Promise verbs (queryClient.fetch, ensure, runPromise) reject with the failure itself, the IssueNotFound instance, not a wrapper around it.
  • When a query's first load fails, use() throws the failure to the nearest error boundary, and resetting the boundary refetches.
  • A failed background refetch keeps the old data on screen and puts the failure in controls.error.
  • A loader that rejects renders the route's errorComponent, and an action that rejects throws to the nearest error boundary.

controls.error is also unknown — narrow it the same way:

function 
function IssueTitle(props: {
    handle: QueryHandleWithData<typeof issueQuery>;
}): React.JSX.Element
IssueTitle
(
props: {
    handle: QueryHandleWithData<typeof issueQuery>;
}
props
: {
handle: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
: interface QueryHandleWithData<TFactory extends QueryFactory = (input: unknown) => FactoryResult>QueryHandleWithData<typeof
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
> }) {
const [const issue: Issueissue,
const controls: QueryControls<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
controls
] =
useQuery<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, true>(factory: (input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>, handle: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>, options?: (Options & ... 1 more ... & {
    ...;
}) | undefined): readonly [data: ...] (+1 overload)
useQuery
(
function issueQuery(input: {
    id: number;
}): QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>
issueQuery
,
props: {
    handle: QueryHandleWithData<typeof issueQuery>;
}
props
.
handle: QueryHandleWithData<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
handle
, {
throwOnError?: true | undefinedthrowOnError: true, }); return ( <React.JSX.IntrinsicElements.header: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>header> <React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1>{const issue: Issueissue.Issue.title: stringtitle}</React.JSX.IntrinsicElements.h1: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>h1> {import SchemaSchema.const is: <typeof Unauthorized>(schema: typeof Unauthorized) => <I>(input: I) => input is I & Unauthorizedis(class UnauthorizedUnauthorized)(
const controls: QueryControls<(input: {
    id: number;
}) => QueryOptions<Flat<{
    data: Issue;
    key: readonly ["issue", number];
} & {
    error: IssueNotFound | Unauthorized;
} & {
    services: IssueApi;
}>>>
controls
.QueryControls<(input: { id: number; }) => QueryOptions<Flat<{ data: Issue; key: readonly ["issue", number]; } & { error: IssueNotFound | Unauthorized; } & { services: IssueApi; }>>>.error: unknownerror) ? (
<React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p React.HTMLAttributes<HTMLParagraphElement>.role?: React.AriaRole | undefinedrole="status">Signed out. Showing the last copy.</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>p> ) : null} </React.JSX.IntrinsicElements.header: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>header> ); }

A query that failed during server rendering rejects in the browser with a RedactedError, unless the integration sets keepErrors (SSR and hydration), so Schema.is is false for it. The fallback branch covers it, and resetting the boundary refetches from the browser, where the failure keeps its type.