/** * Typescript class based component for custom-error * @link https://nextjs.org/docs/advanced-features/custom-error-page */ import { NextPage, NextPageContext } from "next"; import NextError, { ErrorProps } from "next/error"; import React from "react"; import { HttpError } from "@lib/core/http/error"; import logger from "@lib/logger"; import { ErrorPage } from "@components/error/error-page"; // Adds HttpException to the list of possible error types. type AugmentedError = (NonNullable & HttpError) | null; type CustomErrorProps = { err?: AugmentedError; message?: string; hasGetInitialPropsRun?: boolean; } & Omit; type AugmentedNextPageContext = Omit & { err: AugmentedError; }; const log = logger.getChildLogger({ prefix: ["[error]"] }); export function getErrorFromUnknown(cause: unknown): Error { if (cause instanceof Error) { return cause; } if (typeof cause === "string") { // @ts-expect-error https://github.com/tc39/proposal-error-cause return new Error(cause, { cause }); } return new Error(`Unhandled error of type '${typeof cause}''`); } const CustomError: NextPage = (props) => { const { statusCode, err, message, hasGetInitialPropsRun } = props; if (!hasGetInitialPropsRun && err) { // getInitialProps is not called in case of // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass // err via _app.tsx so it can be captured // eslint-disable-next-line @typescript-eslint/no-unused-vars const e = getErrorFromUnknown(err); // can be captured here // e.g. Sentry.captureException(e); } return ; }; /** * Partially adapted from the example in * https://github.com/vercel/next.js/tree/canary/examples/with-sentry */ CustomError.getInitialProps = async ({ res, err, asPath }: AugmentedNextPageContext) => { const errorInitialProps = (await NextError.getInitialProps({ res, err, } as NextPageContext)) as CustomErrorProps; // Workaround for https://github.com/vercel/next.js/issues/8592, mark when // getInitialProps has run errorInitialProps.hasGetInitialPropsRun = true; // If a HttpError message, let's override defaults if (err instanceof HttpError) { errorInitialProps.statusCode = err.statusCode; errorInitialProps.title = err.name; errorInitialProps.message = err.message; errorInitialProps.err = err; } if (res) { // Running on the server, the response object is available. // // Next.js will pass an err on the server if a page's `getInitialProps` // threw or returned a Promise that rejected // Overrides http status code if present in errorInitialProps res.statusCode = errorInitialProps.statusCode; log.debug(`server side logged this: ${err?.toString() ?? JSON.stringify(err)}`); log.info("return props, ", errorInitialProps); return errorInitialProps; } else { // Running on the client (browser). // // Next.js will provide an err if: // // - a page's `getInitialProps` threw or returned a Promise that rejected // - an exception was thrown somewhere in the React lifecycle (render, // componentDidMount, etc) that was caught by Next.js's React Error // Boundary. Read more about what types of exceptions are caught by Error // Boundaries: https://reactjs.org/docs/error-boundaries.html if (err) { log.info("client side logged this", err); return errorInitialProps; } } // If this point is reached, getInitialProps was called without any // information about what the error might be. This is unexpected and may // indicate a bug introduced in Next.js new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`); return errorInitialProps; }; export default CustomError;