In Apollo Client 4, with ApolloError removed, how do you tell a GraphQL error from a network error in a query's error?
answer
- one error property, several classes
- static is() instead of instanceof
- errors array inside the response body
- HTTP status and raw body text
- fetch rejections pass through as they are
basics
~10 sApollo Client 4 reports an error class instead of ApolloError. CombinedGraphQLErrors.is(error) means the response carried an errors array, read from error.errors. ServerError, ServerParseError or a raw fetch failure means the request itself failed.
solid answer
~40 sApollo Client 4 removed `ApolloError` and its `graphQLErrors`/`networkError` fields. A hook's `error` is now the error itself, and you branch with each class's static `is()`. `CombinedGraphQLErrors.is(error)` means the server answered with an `errors` array; `error.errors` holds the entries with their `message`, `path` and `extensions`. Everything else is a network-side failure. `ServerError` covers a non-2xx answer and exposes `statusCode` and the unparsed `bodyText`. `ServerParseError` means the body was not valid JSON. When `fetch` itself rejects, for example because the connection dropped, that error passes through unchanged, typically a `TypeError`. `UnconventionalError` wraps a thrown value that was not an error. Use `is()` rather than `instanceof`, because the checks are brand-based and survive errors created in another realm.
code
tsx · 17 linesimport { CombinedGraphQLErrors, ServerError, ServerParseError } from "@apollo/client";
export function describeError(error: unknown): string {
if (CombinedGraphQLErrors.is(error)) {
const fields = error.errors.map((e) => e.path?.join(".") ?? "request");
return `The server could not load: ${fields.join(", ")}`;
}
if (ServerError.is(error)) {
return error.statusCode === 401
? "Your session expired. Sign in again."
: `The gateway answered ${error.statusCode}.`;
}
if (ServerParseError.is(error)) {
return "The server sent a response that is not JSON.";
}
return "Network problem. Check your connection and retry.";
}go deeper
Recall the two families, errors inside a GraphQL response versus a request that failed outright, and that Apollo Client 4 reports them as separate error classes.
Explain CombinedGraphQLErrors versus ServerError, ServerParseError and raw fetch failures, what each carries, and why you branch with static is() now that ApolloError is gone.
Show how content type and status decide the class, why expired-token handling must check both ServerError and GraphQL error codes, and how you would port 3.x error handling safely.
Argue for one error-classification helper shared across the app, so that user messages, telemetry and retry decisions agree on what counts as a network versus a GraphQL failure.
## Two families of failure Every failed operation in Apollo Client falls into one of two families, and the distinction decides what the UI should do: - **GraphQL errors.** The server received the request, and its response carries an `errors` array: a validation failure, or a resolver that threw while other fields succeeded. The HTTP exchange worked. - **Network errors.** The request never produced a usable GraphQL response: the connection dropped, a gateway answered with a non-2xx status, or the body was not JSON. On an internal analytics dashboard behind an API gateway, all three kinds show up: a flaky office network drops requests, the gateway answers `401` when the short-lived access token expires, and the `churnForecast` resolver sometimes throws while the rest of the query succeeds. ## What Apollo Client 4 changed Apollo Client 3 wrapped everything in `ApolloError`, with `error.graphQLErrors` and `error.networkError`. **Apollo Client 4 removed `ApolloError`.** The `error` returned by `useQuery` or `useMutation`, the error a promise rejects with, and the `error` an `ErrorLink` handler receives are the error itself, an instance of one of these classes: | Class | Family | When | What it carries | |---|---|---|---| | `CombinedGraphQLErrors` | GraphQL | the response has an `errors` array | `errors`, `data`, `extensions` | | `ServerError` | network | a status of 300 or above on a non-GraphQL body | `statusCode`, `bodyText`, `response` | | `ServerParseError` | network | the body is not valid JSON | `statusCode`, `bodyText`, `response` | | `UnconventionalError` | network | something in the chain threw a non-error value | the original value as `cause` | | a raw `fetch` error | network | `fetch` rejected, for example because the connection dropped | passed through unchanged | `CombinedProtocolErrors` and `LocalStateError` also exist, for multipart subscription transport errors and for local-state resolvers. A dashboard that runs only queries rarely meets them. ## Checking the type Each class has a static **`is()`** method: ```ts if (CombinedGraphQLErrors.is(error)) { // error.errors: the GraphQL error entries } else if (ServerError.is(error)) { // error.statusCode, error.bodyText } ``` `is()` checks a brand that the class stamps on each instance, rather than walking the prototype chain. That makes it more reliable than `instanceof` when an error was constructed in another realm or by a second copy of the library. `ServerError` no longer parses the body, so `bodyText` is the raw string. Parse it yourself if the gateway returns JSON. ## Reading a CombinedGraphQLErrors Once `CombinedGraphQLErrors.is(error)` has narrowed the type, three fields carry the details: - **`errors`**: the error entries exactly as the server sent them, each with a `message` and, when the server provides them, `path`, `locations` and `extensions`. The dashboard maps `path` to the panel that failed. - **`data`**: the `data` from the same response, which may be partial. Whether the hook also exposes it as `data` is up to `errorPolicy`. - **`extensions`**: the response-level extensions, if any. By default the error's `message` joins the entries' messages with newlines. The static `CombinedGraphQLErrors.formatMessage` can be overridden once, at startup, if logs need a different format. Showing raw server messages to users is rarely wise, so map known `extensions.code` values to your own copy. ## Which class a status code produces `HttpLink` decides based on the response's `content-type`: 1. With `application/json` (or no GraphQL content type), a status of 300 or above throws `ServerError` before the body is parsed. A body that does not parse as JSON throws `ServerParseError`. 2. With `application/graphql-response+json`, the body is parsed as a GraphQL response whatever the status. If it contains `errors`, those errors surface as **`CombinedGraphQLErrors`**, not `ServerError`. So a gateway's plain-JSON `401` arrives as `ServerError` with `statusCode === 401`. The same rejection sent as a GraphQL-response body by the GraphQL server arrives as `CombinedGraphQLErrors`, with its code in `extensions`. Code that handles expired tokens should check both. ## Why the distinction matters in the UI - A **network error** means no data arrived. Show a retry affordance, and let `RetryLink` absorb transient drops. - A **GraphQL error** can come with partial data. Whether the hook exposes that data is decided by `errorPolicy`, not by the error class. - An **auth failure** belongs in the link chain, where a refresh can replay the request, not in every component. ## Porting from Apollo Client 3 - Replace `error.graphQLErrors` with `CombinedGraphQLErrors.is(error) && error.errors`. - Replace `error.networkError` with checks for `ServerError`, `ServerParseError` or the raw error. - Replace `onError(({ graphQLErrors, networkError }) => ...)` with `new ErrorLink(({ error }) => ...)`, which receives the single `error`. - Do not copy the `graphQLErrors`/`networkError` shape from other GraphQL clients' error objects. Apollo Client 4 has no such fields.
- In Apollo Client 4, why do the error classes offer a static is() instead of relying on instanceof?`is()` checks a brand the class stamps on each instance, so it works even when an error was constructed in another realm, such as an iframe, or by a second bundled copy of the library. In those cases `instanceof` gives false negatives because the prototype differs. It also serves as a TypeScript type guard, which narrows `error` so that `error.errors` or `error.statusCode` type-check.
- A server answers HTTP 400 with content-type application/graphql-response+json; which Apollo Client 4 error do you get?`HttpLink` parses bodies with that content type as GraphQL responses regardless of status. The `errors` array in the body therefore surfaces as `CombinedGraphQLErrors`, not as `ServerError`. You get `ServerError` for a non-2xx status only when the response uses another content type, such as plain `application/json` from a gateway.
saying these in an interview costs you the question
- Check error.graphQLErrors and error.networkError on the ApolloError the hook returns.
- Every failed Apollo Client 4 operation is wrapped in CombinedGraphQLErrors.
- A 401 from the gateway shows up as a GraphQL error in the errors array.
- ServerError.result holds the parsed JSON body of the failed response.
- instanceof is the reliable way to check which error class you have.