Dylan Tientcheu I build experiences to make your everyday life simpler.

Logging and error management best practices in SSR apps

6 min read 1770

Server-side rendering (SSR) is an application’s ability to preload a website in the server before sending it to the client to be hydrated by your JavaScript bundle. Hydration refers to the process in which JS takes over the static HTML sent by the server and turns it into a dynamic DOM, which can then react to client-side data changes.

SSR adds a whole new layer to web apps, and despite its many benefits, it may cause problems for developers. SSR means that some work is done on the server, which can be a source for bugs; therefore, these errors need to be handled appropriately as they would be in any normal web application.

Because multiple tools exist to help developers build SSR web apps, in this article we are going to focus on the best practices for gracefully handling errors. I’ll also present some examples showcasing how it may be done in Next.js or Nuxt.js.

Best practices for handling errors in SSR apps

An app that handles errors gracefully will enable the flow of known or unknown errors in a controlled manner, which will help the user and developers know that something went wrong without completely blocking the app’s process.

SSR applications have both a client and server side, so we need to know what to do on both in order to have a complete end-to-end error handling system.

Handing SSR errors on the client side

Our client side will deal mostly with the frontend as we know it. The following is a list of ways in which you can handle errors gracefully on your SSR app’s frontend.

Try…catch blocks

Try…catch blocks help catch errors in asynchronous calls. These errors are output in the catch block, and my be used to notify the user of a potential error and even send it up via an error service:

const handleSignUp = async (email) => {
  try {
    await backend.emailVerificationLink(email)
  } catch(error) {
    console.error(error) // you may also 
    setHasErrors(true)
  }
}

Interceptors for network errors

If you are using an HTTP client like Axios, you can easily handle errors in your app using an interceptor, which will perform a particular action based on the error received from a request or during a response:

axios.interceptors.request.use(function (config) {
    // Do something before request is sent
    return config;
  }, function (error) {
    // Do something with request error
    return Promise.reject(error);
  });

// Add a response interceptor
axios.interceptors.response.use(function (response) {
    // Do something with response data
    return response;
  }, function (error) {
    // Do something with response error
    return Promise.reject(error);
  });

More tools for handling client-side errors

Client-side error handling is easy with help of tools like react-error-boundary in React. It helps by providing a wrapper that you can use around your components to gracefully handle any errors that occur in that context:

function ErrorFallback({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
    </div>
  )
}

const ui = (
  <ErrorBoundary
    FallbackComponent={ErrorFallback}
    onReset={() => // reset the state of your app so the error doesn't happen again }}
  >
    <ComponentThatMayError />
  </ErrorBoundary>
)

Vue.js also provides an errorCaptured hook that will work like a charm whenever any component in its hierarchy catches an error. These errors will be channeled to a global error handler, onto which you may attach a logger:

// in component.vue
errorCaptured(err, instance, info) {
  // handle inside component (/!\ caution component state still editable)
}

// in main.ts
Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` is a Vue-specific error info, e.g. which lifecycle hook
  // the error was found in. Only available in 2.2.0+
}

LogRocket: The ultimate solution for handling client-side errors

While using an option like react-error-boundary will surface an error, it won’t give you any insight into why the issue occurred. If you’re interested in automatically surfacing client-side errors, monitoring and tracking Redux state, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Handling SSR errors on the server side

Catching errors in data fetchers

The early return pattern refers to returning values as early as possible to help end a function and get a result.

For example, in Next, you would write your getInitialProps function this way:

import Error from "next/error"

function Page({ stars, statusCode}) {  
  if (statusCode !== 200) {
    <Error statusCode={statusCode} />
  } 
  return <div>Next stars: {stars}</div>
}

Page.getInitialProps = async (ctx) => {
  try{
    const res = await fetch('https://api.github.com/repos/vercel/next.js')
    const json = await res.json()

    if (res.status >= 400) {
      return { stars: json.stargazers_count, statusCode: res.status }
    }
    return { stars: json.stargazers_count, statusCode: 200 }    
  } catch(error) {
    return {stars: null, statusCode: 503}
  }
}

This will ensure that you are able to render Page correctly if the result is correct. If there’s an error during the request, it will be gracefully handled and shown to the user using Next’s Error component.

In Nuxt, there are the fetch and asyncData hooks, which are able to handle errors by themselves. These hooks are all used to fetch data, however, they do not handle the process in a similar manner.

Diagram showing lifecycle of Nuxt hook
Source: Nuxt docs

Handling the errors at the fetching point of the app is definitely one of the most important error management tasks in an SSR application.

Error status codes

There are many recognizable HTTP error codes. Usually, the codes you may want to display to the user are client errors (400-500). Otherwise, every error code should still be caught and logged.

In case your SSR app uses server or API middleware, you need to return the appropriate errors too, because they operate exactly as a backend to your app.

Redirect to error pages

In case an error causes your page to get messy (loading or not found), you always want to have a fallback page or component that will help describe the error to the user and hide any unnecessary technical details that may not help the user fix it.

Next already provides a static 404 page that will pop up anytime a user tries to access a nonexistent page. However, you may need a custom 404 page in case you need to add information, or to suit your branding needs.

This is done by creating a pages/404.js file, which is able to fetch data at build time if required. Note that you can do the same with the status code 500 referring to server errors:

// pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}

// pages/500.js
export default function Custom500() {
  return <h1>500 - Server-side error occurred</h1>
}

If ever you need to have a different error description page based on the the error caught, you can resort to more advanced error page customizations, which work using Next’s error component. You can override it by customizing pages/_error.js

In Nuxt, you are gifted with a special layout defined in a layouts/error.vue file. This layout is basically a Vue component with the ability to catch errors along with their status codes. This gives developers a great amount of flexibility:

<!-- layouts/error.vue -->
<template>
  <div>
    <h1 v-if="error.statusCode === 404">Page not found</h1>
    <h1 v-else>An error occurred</h1>
    <NuxtLink to="/">Home page</NuxtLink>
  </div>
</template>

<script>
  export default {
    props: ['error'],
    layout: 'error' // you can set a custom layout for the error page
  }
</script>

Handle and log everything

Because you are in an SSR environment, there’s much more involved than just the user interfaces. Sometimes you deal with middleware and serverMiddlewares in Nuxt or Next.js, which may perform some work on your pages before, during, or after render. These should return appropriate errors, because they act as part of your code.

In certain SSR websites, the server or API middleware act as a full-fledged backend. They may handle API calls like an Express server would. These can easily connect to your logging engine and handle API request errors as they would in Node.

Having your errors handled from end to end will grant you the ability to drill through errors, and find their origin and root cause.

Reporting and logging tips

Once your application gets into a user’s hands, it will be beneficial for you start handling and extracting logs via a log engine. These logs will come with information on almost every interaction on your app, provided you have the correct setup.

Because you can analyze them in real time, you are also able to pull out errors, anomalies, or any other issue that may come from your applications usage.

Today, there are a plethora of log analysis and management tools. For example, you can see here how Next is used with Sentry to catch and handle errors.

Most of these existing log management tools integrate seamlessly with LogRocket to provide you with well-detailed session information during error reporting.

Wrapping up

Error handling is one of the most important parts of development. Doing it gracefully requires developers to have a good planning and a solid understanding of how errors flow in their web apps.

SSR apps are no less than web apps with a further server step. These server additions increase the work required to have an elegant end-to-end error handling process in your app. However, modern frameworks give us the ability to handle errors following simple steps that scale effortlessly.

Generally, on the client side, you’d wrap your asynchronous calls in try…catch blocks to stop the browser from trying to handle the error itself. This way, you’ll transform it to something understandable and let it flow to the user and to your log engine. On the server side, you can catch and return errors using conventional codes so your client side has no problem trying to understand what went wrong.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Dylan Tientcheu I build experiences to make your everyday life simpler.

Leave a Reply