Errors
GraphQL error codes are mapped onto typefetch's shared ErrorKind, so one
global handler covers every transport:
client.onError((error) => {
if (error.kind === "unauthenticated") redirectToLogin();
});fires for a GraphQL UNAUTHENTICATED exactly as it does for an HTTP 401.
extensions.code | kind |
|---|---|
UNAUTHENTICATED | unauthenticated |
FORBIDDEN | permission_denied |
BAD_USER_INPUT, GRAPHQL_VALIDATION_FAILED, GRAPHQL_PARSE_FAILED | invalid_argument |
NOT_FOUND | not_found |
TOO_MANY_REQUESTS | resource_exhausted |
INTERNAL_SERVER_ERROR | internal |
SERVICE_UNAVAILABLE | unavailable |
Both response media types are handled: the legacy application/json (errors
delivered inside a 200) and application/graphql-response+json (errors with a
real 4xx/5xx). extensions.http.status is honoured when present.
Typed error bodies
errors is keyed by extensions.code for a GraphQL endpoint, and
isContractError narrows off it:
errors: {
UNAUTHENTICATED: z.object({ code: z.literal("UNAUTHENTICATED"), realm: z.string() }),
}
try {
await api.user.get({ id });
} catch (e) {
if (isContractError(contracts.user.get, e, "UNAUTHENTICATED")) {
e.data.realm; // fully typed
}
}Partial data
A response carrying both data and errors is the case every GraphQL client
gets wrong in one direction or the other.
errorPolicy: "none"(default) — throw. A partially-failed response is a failed one, and silently returning half a result is how anullreaches a UI three screens from its cause.errorPolicy: "all"— resolve the data and hand the errors toonPartialErrors, so the return type staysPromise<T>and nothing is dropped.
graphqlTransport({
url: "…",
errorPolicy: "all",
onPartialErrors: (errors, { endpointId }) => log.warn(endpointId, errors),
});