'How to catch server errors in Nuxt.js so it doesn't crash page render? (Vue)

Context

This question is related to my other question, How to handle apollo client errors crashing page render in Nuxt? , but I'll try to keep this isolated since I'd like this question focused only on Nuxt (minus apollo). However, I decided to ask this separate since I'm looking for an entirely different response/solution.

The problem

I'm currently maintaining a production Nuxt/Vue app that is using the @nuxt/apollo module to make GraphQL requests.

The problem, is that every now and then, the GraphQL server we rely on goes down and returns an HTML error page, which crashes the Apollo client. But because we're loading Apollo as a nuxt module, it crashes the page render pipeline as well. Giving us a generic server error page that looks like this;

page error

Server error An error occurred in the application and your page could not be served. If you are the application owner, check your logs for details.

And the following stack trace:

 ERROR  Network error: Unexpected token < in JSON at position 0                                                            08:11:04

  at new ApolloError (node_modules/apollo-client/bundle.umd.js:92:26)
  at node_modules/apollo-client/bundle.umd.js:1588:34
  at node_modules/apollo-client/bundle.umd.js:2008:15
  at Set.forEach (<anonymous>)
  at node_modules/apollo-client/bundle.umd.js:2006:26
  at Map.forEach (<anonymous>)
  at QueryManager.broadcastQueries (node_modules/apollo-client/bundle.umd.js:2004:20)
  at node_modules/apollo-client/bundle.umd.js:1483:29
  at processTicksAndRejections (node:internal/process/task_queues:94:5)

However, none of this stack trace allows us to see where nuxt is throwing the error, so we can handle it.

What we tried

We've exhausted all our options looking into this issue for the past couple of weeks. We first tried to solve it by handling the error directly at Apollo level using all 3 apollo library abstractions's error handling solutions:

  • @nuxt/apollo module
  • vue-apollo
  • apollo-client

If you'd like to read up more on that (even though its kind of irrelevant to this question), you can read more on my original question here

However, right now I'd prefer to know if there's a way to somehow handle these page render errors either by:

  • Making the errors fail silently, so the page still renders as normal
  • Allowing us to redirect to another page.

Since the apollo nuxt module we are using currently isn't working for that, I'd like to know if Nuxt supports some kind of way to handle errors.

It didn't help much that Nuxt's documentation is pretty limited when it comes to error handling. At best, it has information regarding the error pages and how to redirect to the error pages using context.error. But it doesn't have a dedicated page on how to catch common errors. I have a feeling Nuxt hooks could be the answer, but documentation on them is hard to navigate and also sparse.

The most complete information source I found on nuxt error handling was this article, Error handling in NuxtJS, of which nothing suggested worked for us.

Summary

Our nuxt app is crashing when the @nuxt/apollo nuxt module we are using crashes. We'd like to know if there's some kind of standard nuxt way of catching it, or if the only solution possible is just migrating our entire app to not use @nuxt/apollo module and use the ES6 promise syntax and load apollo-client manually into the app as a standalone library that's not deeply integrated into the nuxt lifecycle.



Solution 1:[1]

EDIT: I myself think that the problem lies somewhere in the Vue Apollo plugin or Nuxt Apollo module and how errors are handled there. I would think you can handle the error directly at the Apollo module but that is not possible in SSR.

You have to keep in mind that you probably need another solution for both CSR as well as for SSR.

In short, what happens is that the renderRoute fails and because of this SSR ends up in the default errorMiddleware of Nuxt.

1: Calling Apollo query directly, this gives you full control over the error handling for that page and will work for both CSR and for SSR

export default {
  mounted() {
    if (!this.books.length) {
      // client side
      this.fetchBooks()
    }
  },

  serverPrefetch() {
    this.fetchBooks()
  },
  methods: {
    fetchBooks() {
      this.$apollo
        .query({
          query: gql`
            query books {
              books {
                title
                author
                test
              }
            }
          `,
        })
        .catch((e) => {
          console.log(e)
        })
        .then((data) => {
          /// set books
        })
    },
  },
}

2: Add a errorMiddleware hook and handle the error there. This only works for SSR. Important to understand is that rendering failed so you have to redirect or render another page.

//nuxt.config.js
 hooks: {
    render: {
      errorMiddleware(app) {
        app.use((error, req, res, next) => {
          res.writeHead(307, {
            Location: '/network-error',
          })
          res.end()
        })
      },
    },
  },

3: Return false in the error method of Apollo, This only works for CSR

export default {
  apollo: {
    books: {
      query() {
        return gql`
          query books {
            books {
              title
              author
              test
            }
          }
        `
      },
      error() {
        return false
      },
    },
  },
}

Solution 2:[2]

To prevent pages from being crashed, you need to handle errors and isolate them so they won't affect the rendering of other components. This can be achieved with the error handling concept of ErrorBoundary. You can create a common component to reuse the ErrorBoundary logic. There are several other benefits of this approach.

  • Helps to keep components free from error handling logic
  • Allows us to use declarative component composition instead of relying on imperative try/catch
  • We can be as granular as we want with it — wrap individual components or entire application pieces.

Here is the example of how to create an error boundary.

export default {
  name: 'ErrorBoundary',
  data: () => ({
    error: false
  }),
  errorCaptured (err, vm, info) {
    this.error = true
  },
  render (h) {
    return this.error ? h('p', 'Something went wrong') : this.$slots.default[0]
  }
}

Now, you can wrap any component to the error boundary and isolate the error as given,

<error-boundary>
  <counter />
</error-boundary>

Caveats of ErrorBoundary component

There are some caveats when utilizing the errorCaptured hook. Currently, errors are only captured in:

  • render functions
  • watcher callbacks
  • lifecycle hooks
  • component event handlers

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Shea Hunter Belsky