- Published on
A workaround for 'message is omitted in production builds' in RSC errors
As per the Next.js docs on error handling, errors occurring in your server components are passed to your error.tsx
file - a client-side component. There, it's suggested that you should "log the error to an error reporting service" :
'use client' // Error boundaries must be Client Components
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
...
}
Now, that struck me as a bit odd... Why not log errors directly in your server component? Also, what if you end up showing sensitive data to your users?
Well, although the Next.js docs don't mention it, React takes care of the sensitive part - in your production bundle, all errors sent this way get sanitized automatically, so the actual error message always gets replaced with this generic one:
An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.
Still - none of the above matches what I would actually need to happen in case of an error in an actual production app:
On the server side:
- populate a custom error context, including potentially sensitive data
- generate a unique reference number to identify the error
- log the error - the custom error context, the unique reference number, additional telemetry etc
- whenever possible, build a context-aware user-friendly error message that might help the user recover from the error
- make the sanitized error data (reference number + user-friendly message) available to the end-user
On the client side:
- show the user-friendly error message, so users can attempt to recover
- show the server-generated unique reference number, so users can append it to support requests
Workaround
The only solution I found for now is to override the digest
field (which is simply a string React generates by running the error message and stack trace through a hashing function) - this is the only part that React will leave untouched on both dev and production environments. We'll instead use it as a delimited string - that should get both the error id and the user-friendly error message to our error.tsx
:
On the server side:
export const PUBLIC_ERROR_DELIMITER = "|";
export type PublicErrorId = `${number}-${string}`;
export type PublicErrorMessage =
`${PublicErrorId}${typeof PUBLIC_ERROR_DELIMITER}${string}`;
export type PublicError = Error & {
digest?: PublicErrorMessage;
};
export class AppError extends Error {
public readonly publicErrorId: PublicErrorId;
public readonly publicMessage: string;
public digest: string;
constructor({
error,
internalMessage,
publicMessage,
}: {
error?: unknown;
internalMessage?: string;
publicMessage?: string;
}) {
const publicErrorId = generateErrorId();
const publicMessageOrDefault = publicMessage ?? "Something went wrong";
const pb: PublicErrorMessage = `${publicErrorId}${PUBLIC_ERROR_DELIMITER}${publicMessageOrDefault}`;
super(pb);
this.digest = pb;
this.publicErrorId = publicErrorId;
this.publicMessage = publicMessageOrDefault;
logException(error, publicErrorId, internalMessage);
}
}
if (!orgId) {
throw new AppError({
internalMessage: `No valid organization found for customer ${customerId}.`,
publicMessage: `Some data we needed is missing. Please restart your onboarding.`,
});
}
On the client side:
"use client"; // Error boundaries must be Client Components
export default function Error({
error,
reset,
}: {
error: PublicError;
reset: () => void;
}) {
// parse the public error here - extract error id, user-friendly message.
return (
<div>
<!-- show the error id and the user-friendly message here -->
</div>
);
}