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>;
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;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); 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 [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 });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>;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:
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:
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: "" });
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, theIssueNotFoundinstance, 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.