@jpbberry/react-expected-errors
v0.0.1
Published
Create and handle expected errors from React server functions in development and production mode.
Readme
react-expected-errors
Ever want to just throw an error from the server, but you're using react, so you're just, not allowed?
This package adds a simple way to catch expected errors, with all of their data, INCLUDING IN PRODUCTION!
React does a funny thing and hides all information for an error in production mode, for safety. It should be configurable, but it's not.
The generally accepted answer was to take the error and send it down rather than doing any kind of catch logic. But this is pretty dirty, and annoying, especially when dealing with inner functions.
This package takes over the digest parameter to create a parseable representation of the error object, so that the front-end code can still read the information. But security is not fully gone! It does not work with every error of course, instead you pass a list of errors to the main function, allowing you to keep tracking of "user-facing errors." Especially those that come with the sole purpose of presenting error information in a friendly way.
Installation
Install via npm i @jpbberry/react-expected-errors
This package does NOT work on React 18, you must install React ^19
How-to
First you must create your errors, this does some reflection magic and makes your errors exposable!
/src/errors.tsx
import { createErrors } from "@jpbberry/react-expected-errors";
const errors = createErrors({
UserError: class extends Error {
abc = "def";
},
AnotherError: class extends Error {
def = "ghi";
},
});
export const { UserError, AnotherError } = errors.errors;
export const createHandler = errors.createHandler;Then use the createHandler function exposed here, to manage your boundary.
/src/app/error.tsx
"use client"; // important
import { createHandler, UserError } from "../errors.tsx";
export default createHandler((error) => {
if (error instanceof UserError) {
// fully typed!
return (
<p>
Whoopsies user! {error.message} ({error.abc})
</p>
);
}
return <p>Unknown error</p>;
});All done, error handling is now setup!
Example
Now we can just simple throw this error from a server component / action, and it will just simply work how you would expect it
/src/server/actions.ts
"use server";
import { UserError } from "../errors.tsx";
export async function getData() {
throw new UserError("abcdef");
// this will error any static rendering btw, you can't by default throw an error, make it parameter based
}/src/app/page.tsx
import { getData } from "../server/actions";
export default async function Page() {
const data = await getData();
// ...
}The getData() will throw the error, causing the error boundary to throw. And the createHandler inner-function will take care of parsing out the function type!
This of course also works in any boundary of the app, I usually end up just adding export { default as default } from 'app/error.tsx' in every error.tsx, because it's easy to have centralized error looks now!
Security
Please be very very careful with this package, it may not seem like much can happen, but exposing errors can lead to some unintended security issues.
Tips:
- Only create pure-classes inside of
createErrors() - Do not include error classes defined anywhere else
- Do not extend error classes you do not own, or be extra safe and only use
Error - Do not accept data structures you do not control as properties of your errors
- Do not interact with system APIs inside of an error class
Error Stack
By default error stacks are removed from the list of properties that will get sent to the client. Doing so makes it less like a data-leak could occur. Notably stacks can contain causes of other errors you do not control, which can hold data you do not control. Stacks also expose the file system paths if that's a worry as well.
If you absolutely know what you're doing, you can include stack traces by doing the following:
const errors = createErrors({ ... }, { includeStack: true })