Skip to content

Error Handling ​

Apollo Client surfaces failures in a uniform error field. This page covers how to read those errors, classify them, and recover.

Categories of errors ​

Errors fall into two broad categories: GraphQL errors and network errors.

GraphQL errors ​

These come from the server-side execution of a GraphQL operation:

  • Syntax errors (the query was malformed).
  • Validation errors (the query referenced a field that does not exist).
  • Resolver errors (a field's resolver threw during execution).

If a syntax or validation error occurs, the server does not execute the operation at all. If a resolver error occurs, the server may still return partial data.

A GraphQL error response looks like:

json
{
  "errors": [
    {
      "message": "Cannot query field \"nonexistentField\" on type \"Query\".",
      "locations": [{ "line": 2, "column": 3 }],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED"
      }
    }
  ],
  "data": null
}

Apollo Client represents GraphQL errors with the CombinedGraphQLErrors class.

Network errors ​

These come from communicating with the server:

  • 4xx or 5xx HTTP status codes.
  • Network unavailable.
  • Response parsing failures (invalid JSON).
  • Custom errors thrown in your Apollo Link chain.

Network errors are represented by several classes:

ClassWhen it appears
ServerErrorServer responded with a non-200 HTTP status.
ServerParseErrorServer response could not be parsed as JSON.
CombinedProtocolErrorsTransport-level errors during multipart HTTP subscriptions.
LocalStateErrorErrors in local-state configuration or execution.

Reading errors ​

Queries ​

Read errors through current.error:

vue
<script setup lang="ts">
const { 
current
} =
useQuery
(
gql
`
query GetUsers { users { id } } `) </script> <template> <
div
v-if="
current
.
loading
">
Loading... </
div
>
<
div
v-else-if="
current
.
error
"
class
="error">
Error: {{
current
.
error
.
message
}}
</
div
>
<
div
v-else-if="
current
.
resultState
=== 'complete'">
<!-- Render data --> </
div
>
</template>

The #error slot covers the case where the query failed and there is nothing to show. It receives refetch, so the retry lives in the template:

vue
<script setup lang="ts">
import { 
ApolloQuery
} from '@vue/apollo-components'
</script> <template>
<
ApolloQuery
:query
=
"
GetUsers
"
>
<template #
loading
>
Loading... </template> <template #
error
="{
error
,
refetch
}">
<
p
class
="error">
Error: {{
error
.
message
}}
</
p
>
<
button
@
click
="
refetch
()">
Try again </
button
>
</template> <template #
data
="{
data
}">
{{
data
.
users
.
length
}} users
</template> </ApolloQuery> </template>

Slot resolution is data-first, so a refetch that fails over data already on screen keeps rendering #data rather than replacing a working page with an error. Those failures reach the @error event, where a toast is usually the right treatment. See Two ways to read the result.

Mutations ​

Read errors through the error ref:

vue
<script setup lang="ts">
const { 
mutate
,
error
,
loading
} =
useMutation
(
gql
`
mutation CreateUser($name: String!) { createUser(name: $name) { id } } `) </script> <template> <
p
v-if="
error
"
class
="error">
{{
error
.
message
}}
</
p
>
</template>

error is a slot prop on the default slot:

vue
<script setup lang="ts">
import { ApolloMutation } from '@vue/apollo-components'
import { CreateUser } from './queries'
</script>

<template>
  <ApolloMutation
    v-slot="{ mutate, loading, error }"
    :mutation="CreateUser"
    @error="failure => console.error('Create failed:', failure)"
  >
    <button :disabled="loading" @click="mutate({ variables: { name: 'Alice' } })">
      Create
    </button>
    <p v-if="error" class="error">
      {{ error.message }}
    </p>
  </ApolloMutation>
</template>

<ApolloMutation> bridges @error to onError only while the parent is actually listening, so binding the event is what makes mutate() resolve here. Leave @error unbound and the same call rejects instead, exactly as it would under useMutation's throws: 'auto'. See Error throwing behavior.

Subscriptions ​

Read errors through the error ref, or register an onError callback:

vue
<script setup lang="ts">
const { 
error
,
onError
} =
useSubscription
(
gql
`
subscription OnMessageAdded { messageAdded { id } } `)
onError
((
err
) => {
console
.
error
('Subscription error:',
err
)
}) </script> <template> <
div
v-if="
error
">
Connection error: {{
error
.
message
}}
</
div
>
</template>

Read errors from the error slot prop, next to the restart that reconnects:

vue
<script setup lang="ts">
import { 
ApolloSubscription
} from '@vue/apollo-components'
</script> <template> <
ApolloSubscription
v-slot
="{
error
,
restart
}"
:subscription
="
OnMessageAdded
">
<
div
v-if="
error
">
Connection error: {{
error
.
message
}}
<
button
@
click
="
restart
()">
Reconnect </
button
>
</
div
>
</ApolloSubscription> </template>

The same failure is also emitted as @error, which is what Error event hooks below covers.

Error event hooks ​

Every composable exposes an onError event hook for imperative handling:

ts
const { 
onError
} =
useQuery
(
GET_USERS
)
onError
((
error
) => {
console
.
error
('Query failed:',
error
.
message
)
// Show a toast notification // toast.error(error.message) // Report to an error-tracking service // errorTracker.capture(error) })

<ApolloQuery>, <ApolloMutation>, <ApolloSubscription> and <ApolloSubscribeToMore> emit @error. <ApolloFragment> has none.

vue
<script setup lang="ts">
import type { ErrorLike } from '@apollo/client'
import { ApolloQuery } from '@vue/apollo-components'
import { GetUsers } from './queries'

function report(error: ErrorLike) {
  console.error('Query failed:', error.message)

  // Show a toast notification
  // toast.error(error.message)

  // Report to an error-tracking service
  // errorTracker.capture(error)
}
</script>

<template>
  <ApolloQuery :query="GetUsers" @error="report">
    <template #data="{ data }">
      {{ data.users.length }}
    </template>
  </ApolloQuery>
</template>

Unlike #error, the event fires for every failure, including one that lands on top of a result the reader is still looking at. That makes it the right place for logging and toasts, and #error the right place for the empty-page treatment.

Mutation throwing behavior ​

By default, mutate() throws when no onError handler is registered. The throws option controls this:

ts
// Never throw, read the error ref instead
const { 
mutate
,
error
} =
useMutation
(
CREATE_USER
, {
throws
: 'never' })
async function
handleSubmit
() {
await
mutate
({
variables
: {
name
: 'Alice' } })
if (
error
.
value
) {
console
.
error
('Failed:',
error
.
value
.
message
)
} }

Or with throws: 'always' and a try/catch:

ts
const { 
mutate
} =
useMutation
(
CREATE_USER
, {
throws
: 'always' })
async function
handleSubmit
() {
try { const
result
= await
mutate
({
variables
: {
name
: 'Alice' } })
console
.
log
('Created user:',
result
?.
data
?.
createUser
)
} catch (
e
) {
console
.
error
('Mutation failed:',
e
)
} }
ValueBehavior
'auto'Throws when no onError handler is registered (default)
'always'Always throws
'never'Never throws

<ApolloMutation> has the same default. Binding @error is what registers a handler under 'auto', so mutate() resolves and the failure arrives on the event; with nothing bound it rejects. throws has no dedicated prop, so set it through options, which takes the full useMutation.Options object:

template
<ApolloMutation :mutation="CreateUser" :options="{ throws: 'always' }" />

See Error throwing behavior for the full table.

Error policies ​

By default, Apollo Client discards partial data when a GraphQL error occurs and populates error. Change this with errorPolicy:

ts
const { 
current
} =
useQuery
(
GET_USERS
, {
errorPolicy
: 'all',
})

errorPolicy has no dedicated prop; set it through options, which takes the full useQuery.Options object:

template
<ApolloQuery :query="GetUsers" :options="{ errorPolicy: 'all' }">
PolicyBehavior
'none'Returns errors in error and clears result. (default)
'ignore'Ignores errors; error is not populated. result may still hold partial data.
'all'Populates both result and error so you can render both.

Using partial data ​

With errorPolicy: 'all', partial data is available alongside the error:

vue
<script setup lang="ts">
const { 
current
} =
useQuery
(
gql
`
query MixedResults { goodField # Resolves successfully badField # Resolver throws } `, {
errorPolicy
: 'all' },
) </script> <template> <
div
v-if="
current
.
error
"
class
="warning">
Some data couldn't be loaded: {{
current
.
error
.
message
}}
</
div
>
<
div
v-if="
current
.
result
">
<
p
>Good field: {{
current
.
result
.
goodField
}}</
p
>
</
div
>
</template>

#data types its data prop as a complete result, so a partial one would be handed to you under a type that promises fields it does not have. Use the default slot instead, where resultState narrows result honestly, exactly as it does in script:

vue
<script setup lang="ts">
import { 
ApolloQuery
} from '@vue/apollo-components'
</script> <template> <
ApolloQuery
v-slot
="{
result
,
resultState
,
error
}"
:query
="
MixedResults
"
:options
="{
errorPolicy
: 'all' }"
> <
div
v-if="
error
"
class
="warning">
Some data couldn't be loaded: {{
error
.
message
}}
</
div
>
<
p
v-if="
resultState
!== 'empty'">
Good field: {{
result
.
goodField
}}
</
p
>
</ApolloQuery> </template>

The two modes mix, so a #data slot can still handle the everything-worked case while the default slot renders the warning banner above it.

Identifying error types ​

Apollo Client's error classes expose a static is method that identifies the error reliably across realms (workers, iframes, etc.). Prefer it over instanceof:

ts
import {
  CombinedGraphQLErrors,
  CombinedProtocolErrors,
  ServerError,
  ServerParseError,
} from '@apollo/client/errors'

function handleError(error: unknown) {
  if (CombinedGraphQLErrors.is(error)) {
    console.log('GraphQL errors:', error.errors)
  }
  else if (ServerError.is(error)) {
    console.log('Server error:', error.statusCode)
  }
  else if (ServerParseError.is(error)) {
    console.log('Parse error:', error.bodyText)
  }
  else if (CombinedProtocolErrors.is(error)) {
    console.log('Protocol errors:', error.errors)
  }
  else {
    console.log('Unknown error:', error)
  }
}

For app-wide error handling, ErrorLink intercepts every operation:

ts
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'
import { CombinedGraphQLErrors } from '@apollo/client/errors'
import { ErrorLink } from '@apollo/client/link/error'

const errorLink = new ErrorLink(({ error }) => {
  if (CombinedGraphQLErrors.is(error)) {
    error.errors.forEach(({ message, locations, path }) => {
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`,
      )
    })
  }
  else {
    console.error('[Network error]:', error)
  }
})

const httpLink = new HttpLink({ uri: '/graphql' })

const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: from([errorLink, httpLink]),
})

Retry on authentication errors ​

ErrorLink can refresh a token and retry the operation:

ts
import { CombinedGraphQLErrors } from '@apollo/client/errors'
import { ErrorLink } from '@apollo/client/link/error'

const errorLink = new ErrorLink(({ error, operation, forward }) => {
  if (CombinedGraphQLErrors.is(error)) {
    for (const err of error.errors) {
      if (err.extensions?.code === 'UNAUTHENTICATED') {
        const newToken = refreshToken()

        const oldHeaders = operation.getContext().headers
        operation.setContext({
          headers: {
            ...oldHeaders,
            authorization: `Bearer ${newToken}`,
          },
        })

        return forward(operation)
      }
    }
  }
})

ErrorLink retries an operation only once

If the retried operation also fails, those errors do not reach ErrorLink again.

Retry on network errors ​

For network errors, RetryLink handles exponential backoff:

ts
import { from, HttpLink } from '@apollo/client'
import { RetryLink } from '@apollo/client/link/retry'

const retryLink = new RetryLink({
  delay: {
    initial: 300,
    max: Infinity,
    jitter: true,
  },
  attempts: {
    max: 5,
    retryIf: (error, _operation) => !!error,
  },
})

const link = from([retryLink, new HttpLink({ uri: '/graphql' })])

Resetting mutation errors ​

reset() clears mutation errors so the form can be tried again:

vue
<script setup lang="ts">
const { 
mutate
,
error
,
reset
} =
useMutation
(
CREATE_USER
)
function
dismissError
() {
reset
()
} </script> <template> <
div
v-if="
error
"
class
="error">
{{
error
.
message
}}
<
button
@
click
="
dismissError
">
Dismiss </
button
>
</
div
>
</template>

reset() is a slot prop:

vue
<script setup lang="ts">
import { ApolloMutation } from '@vue/apollo-components'
import { CreateUser } from './queries'
</script>

<template>
  <ApolloMutation v-slot="{ mutate, error, reset }" :mutation="CreateUser" @error="console.error">
    <button @click="mutate({ variables: { name: 'Alice' } })">
      Create
    </button>
    <div v-if="error" class="error">
      {{ error.message }}
      <button @click="reset()">
        Dismiss
      </button>
    </div>
  </ApolloMutation>
</template>

See Resetting state for what else it clears.

Next steps ​

Released under the MIT License.