In order to have a good user experience and proper error handling with meaningful logs, we need to follow some guidelines.
Note
The best way to learn how to handle errors is to look at the existing code and see how it's done.
- ✅ Always use a
try/catch
block to catch errors and send a proper error response. - Everything that can throw an error should be inside the
try
block. - Everything thrown error will be caught and handled in the
catch
block.
- ✅ Always return a
json(data({...}))
response in thetry
block. - ✅ Always throw a
json(error({...}))
response in thecatch
block.
export function loader(){
try {
// Do something
return json(data({name: 'John'}));
} catch (cause) {
const reason = makeShelfError(cause);
throw json(error(reason));
}
}
export default Route() {
const loaderData = useLoaderData<typeof loader>();
// ^ {name: string}
}
- ✅ Always return a
json(data({...}))
response in thetry
block. - ✅ Always return a
json(error({...}))
response in thecatch
block.
Now, in the route component using useActionData
, you can access the returned data or error.
You have to handle the error first before accessing the data.
export function action(){
try {
// Do something
return json(data({name: 'John'}));
} catch (cause) {
const reason = makeShelfError(cause);
return json(error(reason));
}
}
export default Route() {
const actionData = useActionData<typeof action>();
// ^ {error: {...} | null} | {name: string, error: null}
const data = actionData && !actionData.error ? actionData.data : null;
// ^ {name: string, error: null} | null
const error = actionData?.error;
// ^ {...} | undefined
}
- ✅ Always use a
try/catch
block to catch errors and send a proper error response. - Everything that can throw an error should be inside the
try
block. - Everything thrown error will be caught and handled in the
catch
block.
- ✅ Always return a
json(data({...}))
response in thetry
block. - ✅ Always return a
json(error({...}))
response in thecatch
block.
- ✅ Always return a
json(data({...}))
response in thetry
block. - ✅ Always return a
json(error({...}))
response in thecatch
block.
Important
Only throw ShelfError
, never a json
or Response
- ✅ Always use a
try/catch
block to catch errors and send a properShelfError
. - Everything that can throw an error should be inside the
try
block. - Everything thrown error will be caught and handled in the
catch
block.
- ✅ Always try to use a
try/catch
block, in a dedicated function, to catch errors and send a properShelfError
. - If you don't want to extract your db query in a function, use
.catch()
to handle any error.
async function loader({ params }: LoaderFunctionArgs) {
try {
const user = await getUser(params.id); // This function handles its own errors
const userMainOrg = db.organization
.findFirst({
where: {
orgId: user.mainOrgId,
},
})
.catch((cause) => {
throw new ShelfError({
cause,
message:
"An error occurred while fetching the user main organization",
additionalData: {
params,
user,
},
label: "Organization",
});
}); // Now we have a better understanding of the error happening here
return json(data({ user, userMainOrg }));
} catch (cause) {
const reason = makeShelfError(cause);
throw json(error(reason));
}
}
This class is used to create a custom error object that can be used to throw errors in the application.
Important
If you don't want an error to be captured by Sentry, you can set the shouldBeCaptured
property to false
.
throw new ShelfError({
cause,
message: "An error occurred while fetching the user main organization",
additionalData: {
params,
user,
},
label: "Organization",
shouldBeCaptured: false, // This error won't be captured by Sentry but will still be logged in the console
});
These functions are used to build the payload response returned by json()
. The data()
function is used to send a successful response, while the error()
function is used to send an error response.
This function is used to create a ShelfError
object from a caught error. It is used to standardize the error object and make sure that the error is properly formatted before being sent to the client.
It pairs with the error()
.
It can take an optional additionalData
parameter to add more context to the error.
...
} catch (cause) {
const reason = makeShelfError(cause, {userId});
throw json(error(reason));
}
✅ Use it in a
try/catch
block
This function is used to parse the data coming from a FormData
, URLSearchParams
or an object and validate it against a Zod schema.
It throws a ShelfError
(badRequest()
) if the data is invalid.
Important
By default, errors are not captured by Sentry. If you want to capture the error, you can set the shouldBeCaptured
property to true
.
❌ Don't use it in a
try/catch
block
This function is a superset of the parseData()
function. It is used to parse the params
object and validate it against a Zod schema.
It directly throw a json
response if the params
are invalid.
This function is used to get the validationErrors
from the error.additionalData
object returned by the error()
function.
It pairs well with Forms validation, when you want to display a specific error message for a given field.
const nameError = getValidationErrors<typeof MySchema>(actionData?.error).name
?.message;
This rules will require you to handle floating promises (promises that are not awaited or returned).
This mostly to prevent calling an async function that doesn't internally handle its own errors. This could result in a server crash.
Tip
If you know what you are doing (like calling a sendEmail
function that handles its own errors in a catch block), you can silence this error with calling the function with void
. (Use with caution!)
void sendEmail();