Solution HERE!
I found the solution to handling expected errors in React server actions in this article:
https://joulev.dev/blogs/throwing-expected-errors-in-react-server-actions
Issue:
When handling server actions, simply using throw new Error() to manage expected errors (like wrong passwords or usernames already taken) isn't effective in production. During development (yarn dev), errors are passed to the frontend as expected, but in production (yarn start), you'll encounter this error message:
Error: 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 in this error instance which may provide additional details about the nature of the error.
This happens because Next.js protects against directly throwing errors in server actions in production. Instead, you should return the error.
Original Server Action Code which Fails to send error to frontend in production:
"use server";
export const createDeposit = async (values: CreateDepositSchemaType, token: string) => {
try {
return await handleServerPost(`${USERS_API}deposits/create`, values, token);
} catch (error: any) {
await handleServerError(error);
throw new Error(error);
}
};
export async function handleServerError(error: any) {
try {
if (error && error.message === "Unauthorized") await logout();
if (axios.isAxiosError(error)) {
const response = error.response;
if (response?.statusText === "Unauthorized" || response?.data.message === "Unauthorized") await logout();
if (response && response.data) {
const { message, statusCode } = response.data;
// Handle specific status code 409
if (statusCode !== 200) {
console.log("Conflict error: ", message);
throw new Error(message);
}
throw new Error(message);
}
if (error.code === "ECONNREFUSED") {
throw new Error("Connection refused. Please try again later or contact support.");
}
} else {
throw new Error("Unknown server error, Please try again later or contact support.");
}
} catch (error: any) {
console.log("CATCHING ERR:", error);
throw new Error(error);
}
}
Updated Server Action Code for Production SOLUTION:
"use server";
export const createDeposit = async (values: CreateDepositSchemaType, token: string) => {
try {
return await handleServerPost(`${USERS_API}deposits/create`, values, token);
} catch (error: any) {
return await handleServerError(error);
}
};
// ERROR HANDLERS
export async function handleServerError(error: any) {
try {
if (error && error.message === "Unauthorized") await logout();
if (axios.isAxiosError(error)) {
const response = error.response;
if (response?.statusText === "Unauthorized" || response?.data.message === "Unauthorized") await logout();
if (response && response.data) {
const { message, statusCode } = response.data;
// Handle specific status code 409
if (statusCode !== 200) {
console.log("Conflict error: ", message);
return { message, statusCode };
}
return { message, statusCode };
}
if (error.code === "ECONNREFUSED") {
return { message: "Connection refused. Please try again later or contact support.", statusCode: 500 };
}
} else {
return { message: "Unknown server error, Please try again later or contact support.", statusCode: 500 };
}
} catch (catchError: any) {
return { message: catchError.message, statusCode: 500 };
}
}
CONCLUSION:
This adjustment ensures that errors are returned properly and can be read by the frontend, even in production.
In a few words: Use return on server actions side, not throw if you want to get the error in the frontend.