Server-side rendering (SSR) refers to the process of preloading a website on the server before sending it to the client to be hydrated by your JavaScript bundle. Hydration refers to the process by which JavaScript 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. Because some work is done on the server, it can be a source for bugs; therefore, these errors need to be handled appropriately as they would be in any normal web application.
There are multiple tools and techniques to help developers build SSR web apps, and in this article, we’ll 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.
An app that handles errors gracefully will enable the flow of known or unknown errors in a controlled manner, which will help both users and developers to 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.
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
blockstry…catch
blocks help catch errors in asynchronous calls. These errors are output in the catch
block and may 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) } }
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); });
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+ }
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 — start monitoring for free.
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.
Handling the errors at the fetching point of the app is definitely one of the most important error management tasks in an SSR application.
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.
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>
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.
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. Most of these existing log management tools integrate seamlessly with LogRocket to provide you with well-detailed session information during error reporting.
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.
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.js 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.