Error Handling

As described in Introduction, if an error occurs during server-side rendering, we have two coping strategies: one is to fall back to the SPA mode, and the other is to display a custom error page.

Fall back to SPA mode

Automatic fallback

This is the default behavior of Vapper. When any error occurs during server rendering, Vapper will fall back to SPA mode, which will send the SPA page to the client. If the error is an error that only occurs on the server side, or if the error is a non-fatal error, that means the user can continue to use our app. This makes sense in some scenarios, such as ordering page, payment page, and other scenarios that emphasize conversion rates.

Special handling of errors in route guards

The goal of vapper is to automatically fall back to SPA mode whenever an error occurs, with one exception: if an error occurs in the asynchronous routing guard, vapper cannot fall back to SPA mode. This is because router.onError cannot catch Promises rejections. So we need to manually try...catch the code inside the routing guard, please read: Capturing errors in routing guards.

Manually fallback Core 0.8.0+

If you choose Custom Server, and you might write your own business middleware, but Vapper can't catch exceptions thrown by user-written business middleware. So Vapper exposes the vapper.fallbackSPA(req, res) function to manually fallback to the SPA mode so that the user can call this method in their own error handling middleware to manually fallback to SPA mode:

The vapper.fallbackSPA() function takes two parameters: the Nodejs native request object req and the response object res. The following is an example of Koa, showing how to manually fallback to SPA mode when an error occurs.

















 
 
 
 
 
 
 
 
 
 
 
 
 
 












const Koa = require('koa')
const app = new Koa()
const Vapper = require('@vapper/core')

async function starter () {
  const vapper = new Vapper({ mode: process.env.NODE_ENV || 'production' })

  const {
    options: {
      port,
      host
    }
  } = vapper

  await vapper.setup()

  // Your error handling middleware
  app.use(async (ctx, next) => {
    try {
      await next()
    } catch (err) {
      // Manually call the vapper.fallbackSPA() function
      ctx.status = 200
      ctx.respond = false
      vapper.fallbackSPA(ctx.req, ctx.res)
    }
  })

  // Business middleware is written here
  // app.use(...)

  app.use((ctx) => {
    ctx.status = 200
    ctx.respond = false
    vapper.handler(ctx.req, ctx.res)
  })

  app.listen(port, host, () => vapper.logger.info(`Server running at: http://${host}:${port}`))
}

starter()

About how to customize Server Please read: Custom Server.

Custom fallback logic Core 0.13.0+

By default, vapper internally uses serve-static to provide a static resource service. When a user request comes in, vapper will provide the file under dist/ as a static resource to the user. You can configure it via configuration#static, all configuration options are: serve-static#options.

In general, this is fine, but we usually have a separate static resource server or CDN, and our nodejs service becomes a server that only serves the dist/index.html file. Since the size of the dist/index.html file is small, we can read the file into memory when the service starts, and file IO will no longer occur when a request comes. To do this, vapper provides the configuration#fallbackSpaHandler option, which allows you to customize the logic for fallback the SPA, an example:

// 1. Read the dist/index.html file generated by the build into memory when the service starts
const spaHTMLContent = fs.readFileSync(path.resolve(__dirname, '../dist/index.html'), 'utf-8')

// vapper.config.js
module.exports = {
  // Other configurations...

  // Custom fallback SPA logic
  fallbackSpaHandler (req, res) {
    // 2. Send the in-memory string directly to the client
    res.setHeader('Content-Type', 'text/html; charset=UTF-8')
    res.end(spaHTMLContent)
  }
}

Custom error page

Of course, if you want the error page to be displayed to the user when the error occurs, it is very simple.

The vm.error property of the root component

Vapper injects the error attribute to the root component instance, which is an error object that holds the error message. So you can decide what to render by checking if this.error exists, as shown in the following code:











 







// Entry file: src/main.js

export default function createApp () {
  // 1. Create a router instance
  const router = createRouter()

  // 2. Create a app instance
  const app = new Vue({
    router,
    render (h) {
      return this.error ? h('h1', 'error') : h(App)
    }
  })

  // 3. return
  return { app, router }
}

Within the render function of the root component, if this.error exists, the custom content is presented to the user, otherwise the application is rendered normally. You can render anything you want, such as the Error.vue component:

import Error from './Error.vue'

export default function createApp () {
  // 1. Create a router instance
  const router = createRouter()

  // 2. Create a app instance
  const app = new Vue({
    router,
    render (h) {
      return this.error ? h(Error, { props: { error: this.error } }) : h(App)
    }
  })

  // 3. return
  return { app, router }
}

The Error Object

this.error exists only on the root component instance, it is an error object:

{
  url: to.path, // The url where the error occurred
  code: 404,    // Error code
  message: 'Page Not Found' // Error message
}

What you need to know is that, in fact, you can assign arbitrary values to this.error at runtime, but the good practice is to give it the Error object with the same structure as the object shown in the code above.

Capturing errors in routing guards

For complex applications, such as applications that require permission control, it is normal to write the appropriate authentication logic in the routing guard, for example:

router.beforeEach(() => {
  // Some logic
})

What if the code in the routing guard throws an error? We need to manually try...catch the code inside the route guard and add the error object to the router instance when an error is caught, for example:






 
 
 
 
 
 
 
 
 
 





 
 







// Entry file: src/main.js

export default function createApp () {
  // 1. Create a router instance
  const router = createRouter()
  router.beforeEach((to, from, next) => {
    try {
      // Some logic
    } catch (e) {
      // When an error occurs, assign the error object to the `router.err` property,
      // the `router.err` property is completely custom, you can also name it `router.error`.
      router.err = e
    }
    next()
  })

  // 2. Create a app instance
  const app = new Vue({
    router,
    render (h) {
      // Show custom error page when `router.err` or `this.error` exists
      return router.err || this.error ? h('h1', 'error') : h(App)
    }
  })

  // 3. return
  return { app, router }
}

Why didn't we use router.onError? This is because router.onError is currently unable to catch Promises rejections. For details, please see: https://github.com/vuejs/vue-router/issues/2833.

Best Practices

Since only errors in asynchronous routing guards will break the vapper's fallback SPA logic, so our best practice is to display custom error pages only when errors occur in routing guards, otherwise use vapper's automatic fallback logic, as shown in the following code:





















 
 
 







// Entry file: src/main.js

export default function createApp () {
  // 1. Create a router instance
  const router = createRouter()
  router.beforeEach((to, from, next) => {
    try {
      // Some logic
    } catch (e) {
      // When an error occurs, assign the error object to the `router.err` property,
      // the `router.err` property is completely custom, you can also name it `router.error`.
      router.err = e
    }
    next()
  })

  // 2. Create a app instance
  const app = new Vue({
    router,
    render (h) {
      // Display the custom error pages only when `router.err` exists.
      // We don't care about `this.error`.
      return router.err ? h('h1', 'error') : h(App)
    }
  })

  // 3. return
  return { app, router }
}

As shown in the highlighted code above, the custom error page is only displayed if router.err exists. We don't care about this.error unless you explicitly want to display a custom error page anyway.