error.js removes any possibility to customize the error message #49506
Replies: 24 comments 12 replies
-
The import { notFound } from 'next/navigation';
const Page = async ({
params: { postId },
}) => {
const post = await getPost(id);
if (!post) {
notFound();
}
// ... rest of your page
} As for the first condition You use Docs for |
Beta Was this translation helpful? Give feedback.
-
Thanks but that is not really what I wanted to outline. I have updated my example. Like you said,
What about expected errors that are not 404 ? For instance, if I get a 401 because the auth expired ? Or if I get a 403 because I don't have access to the resource ? I need to write different error messages in these cases. I thought error.js would be my go-to in that situation... |
Beta Was this translation helpful? Give feedback.
-
For unauthorized errors, instead of throwing an error you would have to redirect to the login page (for ex), something like this : import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
const SecretLayout = async ({children}: { children: React.ReactNode }) => {
const user = await getUser(cookies.get('session_token'));
if (!user) {
redirect(`/login`);
}
return <>{children}</>
} I prefer to check for the user in a layout because that Layout can wrap a big part of the ui (a |
Beta Was this translation helpful? Give feedback.
-
Checking the user in the layout is a great idea, but how about the case where you consume an external API that uses JWT to authenticate ? (It's always my case) Let's say we do that in a dashboard, the layout will check for the user session once when the page first load. But your JWT could expire 30 minutes later while you're clicking deep in your UI, leading to a 401 server side ! It was easy to handle that with good old error boundaries, but not anymore with this error.js 😭 Same scenario for 403s : you're in a dashboard, you go to a specific page, choose a specific feature... oh, you don't have the rights to do that. What happens ? To me, that is an expected error that I want to handle. If |
Beta Was this translation helpful? Give feedback.
-
For pages with 403 forbidden you can create another layout where you check if the user can do the action and maybe render an error instead of the page content. import { cookies } from 'next/headers';
import Link from 'next/link';
const SuperAdminLayout = async ({children}: { children: React.ReactNode }) => {
const user = await getUser(cookies.get('session_token'));
const hasAccess = await checkUserAccess(user);
if (!hasAcces) {
return (
<div>
<h1> Error 403 Forbidden, you do not have access to this page</h1>
<Link href={`/dashboard`}>Return to dashboard</Link>
</div>
)
}
return <>{children}</>
} PS: nextjs said that in the future there will be support for conditionnal routes that could make this easier : https://beta.nextjs.org/docs/routing/fundamentals#advanced-routing-patterns For the other case, i would suggest to check in the pages directly if the user is defined, and maybe create a utility function that automatically redirect to the login page for example : // lib/functions.ts
import { cookies } from 'next/headers';
export async function getUserOrRedirect() {
const user = await getUser(cookies.get('session_token'));
if (!user) {
redirect(`/login`);
}
return user;
}
// app/dashboard/protected/page.tsx
import { cookies } from 'next/headers';
const SecretPage = async () => {
const user = await getUserOrRedirect();
// ... your page content
} And for further optimization if you fetch your user at many places in your layouts and server-components in one request, you can use import { cache } from 'react';
export const getUser = cache((session_token: string) => {
// fetch for the api or get the from the DB...
return user;
}); |
Beta Was this translation helpful? Give feedback.
-
I stumbled upon this while trying to "propagate" HTTP Status code through error.js, as right now, it simply always returns HTTP status 200 (I haven't found a way how to make it return e.g. 500). The answers here do not address the issue, but rather recommend work around. FIY, I consider handling this as the very basic property of a web framework, being used to having this in all frameworks starting back in 2005, but what do I know... |
Beta Was this translation helpful? Give feedback.
-
@hnykda I am unhappy too with the workarounds that work for specific use case but when dealing with an external API that has a lot of error case (stripe) please upvote the issue if you agree :D |
Beta Was this translation helpful? Give feedback.
-
I have a similar issue where all requests to our backend includes a error code that needs to be displayed in the UI if the request fails. My current idea is to put this into a shortlived cookie or similar and then read it on the client, but that's definitely a hacky solution. I would really like being able to return a custom error type with data that will be available to the error component, something like this: import { SerializableError } from 'next';
export default function FailingComponent() {
throw new SerializableError('error message', {
// this here is an extra data object that gets json serialized and provided to the error component
errorCode: 15
});
} If the error thrown is a |
Beta Was this translation helpful? Give feedback.
-
I completely agree with the discussion. NextJS has no options to handle the 403 error from the api server. It's very sad. I hope it will be fixed! |
Beta Was this translation helpful? Give feedback.
-
I agree. The solutions showed by Fredkiss are good workarounds, but:
Take for instance a simple form in a server component, using a server action performing a It works using Client components and Can you allow us to opt-out of this configuration? For instance putting a |
Beta Was this translation helpful? Give feedback.
-
Remix has support for caught and uncaught errors on the same function, Next DX is terrible on this matter and the docs lies about all errors being caught by error.js |
Beta Was this translation helpful? Give feedback.
-
In the context of handling errors from server actions, particularly with Prisma (e.g., unique constraint errors), how are others managing to relay informative yet secure error messages to users in a production environment? Given the security measures in Next.js that obscure error details, I'm looking for effective ways to communicate specific errors like these to users, possibly through UI notifications like toasts. Any insights or practices adopted in your projects would be greatly appreciated. |
Beta Was this translation helpful? Give feedback.
-
It would be nice to be able to |
Beta Was this translation helpful? Give feedback.
-
Generally speaking, is someone already able to use the digest code in production in a sensible way? Like forwarding the server error and the digest code to an actual logger or an external service? From what I see, the only place where the digest code appears (other than forwarded to error.js) is the stdout. I know there are ways to collect the stdout logging with third party libraries/services, but that's hacky at best and there's the need to manage it in a more straightforward way. I don't know if discussions/solutions (maybe this in the future) are already in place, but I struggle to find info about this, I just find workarounds about using next-logger or experimental instrumentation hooks (basically monkey patching the console #54719). Very basic use case: I currently have to deploy my app on azure. On error, I would love to just get the actual server error and the digest code and send it to azure application insight (or a pino logger transport, or anything that I can manage in a centralized way), for it to be retrieved at a later time, using the digest code forwarded to the client. (even sentry is struggling with this #60283) |
Beta Was this translation helpful? Give feedback.
-
It was 2023 and now 2024 and next.js doesn't have us handle errors in a typesafe way |
Beta Was this translation helpful? Give feedback.
-
I'm facing this issue also |
Beta Was this translation helpful? Give feedback.
-
I've been struggling with this and didn't want to add a code in all pages we have or add a props.error to the client component and handle it there as I can see in some articles like this one. So I decided to have a wrapper for my pages, here it how it looked like (The ErrorPage basically gets the message and throws it to the user): type ServerComponentFn = ({ params, searchParams }) => Promise<JSX.Element>
export const withErrorCatch =
(loadComponent: ServerComponentFn) =>
async ({ params, searchParams }) => {
try {
const serverComponent = await loadComponent({ params, searchParams })
return serverComponent
} catch (error) {
Sentry.captureException(error)
// we should check if error.digest contains NEXT_REDIRECT as redirect throws an error
if (isNextRedirectError(error as object)) {
throw error
}
return <ErrorPage error={parseError(error as object)} />
}
} and then, on my page.ts, instead of exporting the default page I would just do: export default withErrorCatch(Page) It's not the ideal (I'd rather having a configuration to remove the server omit warning - although I understand the need of this omission), but it's the closest to the ideal I could think of |
Beta Was this translation helpful? Give feedback.
-
I've managed to handle it in the following way (this is not ideal, but it works): we need to have helper function (i.e wrapper): // error-handler.ts
'use server';
import { isDynamicUsageError } from 'next/dist/export/helpers/is-dynamic-usage-error';
export const withErrorHandler = <T extends Array<unknown>, U>(
fn: (...args: T) => Promise<U>,
) => {
return async (...args: T): Promise<U | CustomError> => {
try {
return await fn(...args);
} catch (e) {
if (isDynamicUsageError(e)) throw e;
return { error: { message: (e as Error).message } };
}
};
}; then you should wrap each server action with it, like this: // server/actions.ts
'use server';
export const createFolder = withErrorHandler(
async (params: RequestDto) => {
/// your backend logic, which could potentially throw an exception.
},
); last but not least, you need to have some utils to catch this error on RSC: // utils.ts
export type CustomError = { error: { message: string } };
export const isApiError = <T>(
response: T | CustomError,
): response is CustomError => {
return (
typeof response === 'object' && response !== null && 'error' in response
);
};
export function throwIfError<T>(
response: T,
): asserts response is Exclude<T, CustomError> {
if (isApiError(response)) {
throw new Error(response.error.message);
}
}
export function handleServerAction<T>(response: T): Exclude<T, CustomError> {
throwIfError(response);
return response;
} finally, this is how you call server action in startTransition or RSC try {
handleServerAction(await createFolder(values));
} catch (e) {
showError(e); // e.g shows toast based on e.message.
} |
Beta Was this translation helpful? Give feedback.
-
+1. An improvement to the DX of error handling would be really appreciated. |
Beta Was this translation helpful? Give feedback.
-
In my case calling
But now I am not sure if it will work in production because I tried following the same approach by assigning a custom digest value. Well, it doesn't work, I still get a hashed digest in the end. |
Beta Was this translation helpful? Give feedback.
-
I don't understand this design decision. Let's say I have a SaaS app with a customer that has exceeded its usage for its plan. |
Beta Was this translation helpful? Give feedback.
-
Instead of throwing the error, try returning an object with the error message. I encountered the same issue, and this approach resolved it for me. |
Beta Was this translation helpful? Give feedback.
-
Here's where the error gets modified: https://github.com/facebook/react/blob/a07f5a3db5deb5a429bf2617525b6e66dc777e8c/packages/react-client/src/ReactFlightClient.js#L1711 ===
Server
===
export type ServerActionError = {
isServerActionError: true;
type: string;
message: string;
};
export const updateAccount = async (data: ConnectAccountValues) => {
let result = connectAccountSchema.safeParse(data);
let error: ServerActionError | null = null;
if (!result.success) {
error = {
isServerActionError: true,
type: "parse-error",
message: result.error.message,
};
return error;
}
// Get session
const session = await getServerSession(authOptions);
// Get user
if (!session?.user) {
error = {
isServerActionError: true,
type: "not-signed-in",
message: "Please log in",
};
return error;
}
...
===
Client
===
try {
const result = await updateAccount(data);
if (result && result.isServerActionError) {
throw result;
}
} catch (e: any) {
alert(e.message);
} |
Beta Was this translation helpful? Give feedback.
-
I've found a solution to this First of all, it has to be clear that this behavior is a React thing not Next.js https://github.com/facebook/react/blob/b7e7f1a3fab87e8fc19e86a8088a9e0fe4710973/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js#L980-L983 In production, React omits error messages to mitigate security risks. But it doesn't override the // page.tsx
class CustomError extends Error {
digest = "I'm a custom digest";
constructor() {
super("Custom Error");
this.name = "CustomError";
}
}
export const dynamic = "force-dynamic";
export default function Page() {
if (new Date().getMinutes() >= 48) throw new CustomError();
return "Null";
} // error.tsx
"use client";
export default function Error({
error,
}: { error: Error & { digest: string } }) {
return (
<div dir="ltr">
<p>Message: {error.message}</p>
<p>Digest: "{error.digest}"</p>
</div>
);
} Then, you can rely on the digest to handle your errors, for example // error.tsx
"use client";
export default function Error({
error,
}: { error: Error & { digest: string } }) {
switch (error.digest) {
case "HTTP:400":
return <p>Bad Request</p>;
case "HTTP:401":
return <p>Unauthorized</p>;
case "HTTP:403":
return <p>Forbidden</p>;
// ...
}
return <p>Another error</p>;
} // page.tsx
export const dynamic = "force-dynamic";
export default function Page() {
const error = new Error();
(error as any).digest = "HTTP:401";
if (new Date().getMinutes() >= 5) throw error;
return "Null";
} |
Beta Was this translation helpful? Give feedback.
-
Describe the feature you'd like to request
When using classic error boundaries (like the react-error-boundary package, we have access to the error, which allow us to customize the error message.
For instance let's imagine I have a page /posts/:id.
In my server component I would like to do (very contrived example):
Then retrieve it in the error.js :
However doing that results in the error being transformed into :
This makes us unable to explain errors to users, resulting in bad UX.
Describe the solution you'd like
I would like to be able to opt out of the error behavior, and be able to get the error message.
Describe alternatives you've considered
Maybe we could workaround that by splitting the code in multiple server component and putting a custom error.js for each component, but this would be a hassle !
Beta Was this translation helpful? Give feedback.
All reactions