Janning Vygen
Janning Vygen

Reputation: 9192

How Do I consume errors in my Vue Graphql component and let other errors be handled globally?

We use Vue with apollo and I have difficulties to handle errors properly.

This is on of our components

<template>
   <div v-if="product">
      <router-view :key="product.id" :product="product" />
   </div>
   <div v-else-if="!this.$apollo.loading">
      <p>Product is not available anymore</p>
   </div>
</template>
    
    <script>
import productQuery from "@/graphql/product.graphql";

export default {
   name: "ProductWrapper",
   props: ["productId"],
   apollo: {
      product: {
         query: productQuery,
         variables() {
            return {
               productId: this.productId,
            };
         },
      },
   },
};
</script>

If the product is not available anymore, we have three options in the backend: a) the backend just can send null without errors b) send an error object as part of the data with unions c) send some error extensions with apollo for easy error handling in the client

Option a) seems to be a strange option Option b) is too complicated for our use case. So I decided for option c):

In our backend we use apollo error extensions to send some proper errorCode for the client to handle.

{
   data: { }
   errors: [
      {
         "message": "NOT_FOUND",
         "locations": ...,
         "path": ...
         "extensions": {
         "details": [],
         "errorCode": "NOT_FOUND",
         "message": "NOT_FOUND",
         "classification": "DataFetchingException"
      }
   ]
}

Everything works fine as product results in null anyway as no data is sent, just an error. But vue-apollo is logging this to console.error. I don't want any logging to console.error as the user sees an red mark in his browser. But I want it to pop up in the console.error if nobody else has handled this error.

I can add an error handling in three places:

  1. error() inside query definition
  2. $error() default handler for all queries
  3. ErrorLink

ErrorLink seems to be the wrong place as only the query in the component knows that NOT_FOUND is not fatal but can happen sometimes. Same is true for $error

So how do I say: this error might happen, I am well prepared for this. All other errors should be handled by the ErrorLink. How can I consume an error in my component?

Upvotes: 2

Views: 1516

Answers (1)

Janning Vygen
Janning Vygen

Reputation: 9192

My overview over vue-apollo error handling.

SmartQuery error handler

Documentation: https://apollo.vuejs.org/api/smart-query.html

error(error, vm, key, type, options) is a hook called when there are errors. error is an Apollo error object with either a graphQLErrors property or a networkError property. vm is the related component instance. key is the smart query key. type is either 'query' or 'subscription'. options is the final watchQuery options object.

Not documented: If you return false the error processing is stopped and the default error handler (Apollo Provider) is not called. If you return true or do not return anything (aka undefined) the default error handler is called.

Code Example

export default {
  name: "ProductWrapper",
  props: ['productId'],
  apollo: {
    product: {
        query: productQuery,
        variables() {
            return {
                productId: this.productId
            }
        },
        error(errors) {
          console.log("Smart Query Error Handler")
          console.log(errors.graphQLErrors)
          return false;
        }
    }
  }
}

VueApolloProvider errorHandler

Documentation: https://apollo.vuejs.org/api/apollo-provider.html

Global error handler for all smart queries and subscriptions

Important: This is NOT for mutations.

Not documented: This is not called when an error handler of a smart query returned false

Code Example

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  errorHandler: (errors) => {
    console.log("Provider errorHandler")
    console.log(errors)
  }
});

VueApolloProvider $error special option (useless)

Documentation https://apollo.vuejs.org/guide/apollo/special-options.html

$error to catch errors in a default handler (see error advanced options > for smart queries)

Code Example (wrong)

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  defaultOptions: {
    error(errors) {
      console.log("Provider $error")
      console.log(errors)
    }
  }
});

This was my first try, but it results in a call when the component is mounted and gives us the complete component. See https://github.com/vuejs/vue-apollo/issues/126

Code Example (kind of ok?)

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
  defaultOptions: {
    $error() {
      return (errors) => {
        console.log("Provider $error handler")
        console.log(errors.graphQLErrors)
      }
    },
  },
});

This way the $error function is called. But it is called just like the default errorHandler (see above). the documentation seems to be wrong. So this method looks rather useless to me. As you can see in this debugging screenshot, it is used just like the other error handlers:

debugging screenshot

ErrorLink

Documentation: https://www.apollographql.com/docs/react/data/error-handling/#advanced-error-handling-with-apollo-link

Code example

import {onError} from "apollo-link-error";
const errorHandler = onError(({ networkError, graphQLErrors }) => {
  console.log("Link onError")
  console.log({ graphQLErrors, networkError})
})
const link = split(
  // split based on operation type
  ({query}) => {
    const definition = getMainDefinition(query);
      return definition.kind === 'OperationDefinition' &&
         definition.operation === 'subscription'
    },
   wsErrorHandler.concat(wsLink),
   errorHandler.concat(httpLink)
);

Important: this is called before any other ErrorHandler and it works for queries, subscriptions and mutations

Mutation catch

Documentation https://apollo.vuejs.org/guide/apollo/mutations.html

Code example 1

async submit() {
  await this.$apollo.mutate({
    mutation: productDeleteMutation,
    variables: {
      productId: this.product.id
    },
  }).catch((errors) => {
    console.log("mutation .catch error")
    console.log(errors)
  })
}

Code example 2 You can use try/catch too:

async submit() {
  try {
    await this.$apollo.mutate({
      mutation: productDeleteMutation,
      variables: {
        productId: this.product.id
      },
    })
  } catch (errors) {
    console.log("mutation try/catch error")
    console.log(errors)
  }
}

the only other handler which can be called is the onError ErrorLink (see above), which would be called before the catch error handling.

Mutation handled by vue with errorCaptured

Add this lifecycle hook to your component to catch errors from mutations.

errorCaptured(err, vm, info) {
  console.log("errorCaptured")
  console.log(err.graphQLErrors)
  return false
}

Documentation: https://v2.vuejs.org/v2/api/#errorCaptured

More links: https://medium.com/js-dojo/error-exception-handling-in-vue-js-application-6c26eeb6b3e4

It works for mutations only as errors from smart queries are handled by apollo itself.

Mutation errors with vue global errorHandler

You can have a vue default error handler to catch graphql errors from mutations

import Vue from 'vue';
    
Vue.config.errorHandler = (err, vm, info) => {
  if (err.graphQLErrors) {
    console.log("vue errorHandler")
    console.log(err.graphQLErrors)
  }
};

It works for mutations only as errors from smart queries are handled by apollo itself.

Documentation https://v2.vuejs.org/v2/api/#errorHandler

window.onerror

Errors outside vue can be handled with plain javascript

window.onerror = function(message, source, lineno, colno, error) {
  if (error.graphQLErrors) {
    console.log("window.onerror")
    console.log(error.graphQLErrors)
  }
};

This does not work for mutations, queries or subscriptions inside or outside vue components as these are handle by vue or apollo itself (and printed to console.err if not handled)

Documentation https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror

Summary

The only way to catch errors for mutations AND queries at the same time is onError with an ApolloLink. Server, Network and Authentication errors should be catched there as these are not specific to any operation.

Additional notes

Approach how to handle errors in the backend

https://blog.logrocket.com/handling-graphql-errors-like-a-champ-with-unions-and-interfaces/

Upvotes: 4

Related Questions