Handle Thrown Responses
Run locally for transcripts
๐ฆ As mentioned earlier, Remix allows you to
throw new Response
from your
loaders and actions so you can control the status code and other information
sent in the response. For example:import {
json,
type ActionFunctionArgs,
type LoaderFunctionArgs,
type MetaFunction,
} from '@remix-run/node'
import { invariantResponse, useIsSubmitting } from '#app/utils/misc.tsx'
import { getUser } from '#app/utils/auth.server'
import { getSandwich } from '#app/utils/sandwiches.server'
export async function loader({ request, params }: LoaderFunctionArgs) {
const user = await getUser(request)
if (!user) {
// this response will be handled by our error boundary
throw new Response('Unauthorized', { status: 401 })
}
// this invariant with throw an error which our error boundary will handle as well
invariantResponse(params.sandwichId, 'sandwichId is required')
const sandwich = await getSandwich(params.sandwichId)
if (!sandwich) {
// this response will be handled by our error boundary
throw new Response('Not Found', { status: 404 })
}
return json({ sandwich })
}
We've got a handy
invariantResponse
which we use to throw responses for us
more easily which works just the same way, but for the sake of clarity, most
of these examples throw raw responses.When you throw a
Response
from a loader or action, Remix will catch it and
render your ErrorBoundary
component instead of the regular route component. In
that case, the error
you get from useRouteError
will be the response object
that was thrown.Because it's impossible to know what error was thrown, it can be difficult to
display the correct error message to the user. Which is why Remix also exports a
isRouteErrorResponse
utility which checks whether the error is a Response. If
it is, then you can access the .status
property to know the status code and
render the right message based on that. Your response can also have a body if
you want the error message to be determined by the server.Here's an example of handling a response error:
export function ErrorBoundary() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return <p>Not Found</p>
}
if (error.status === 401) {
return <p>Unauthorized</p>
}
}
return <p>Something went wrong</p>
}
This mechanism of throwing responses is quite powerful because it allows us to
build really nice abstractions. It's exactly what we're doing with the
invariantResponse
utility. As another example, if we didn't like having to do
that user
check everywhere, we could create an abstraction that does it for
us:export async function requireUser(request: Request) {
const user = await getUser(request)
if (!user) {
throw new Response('Unauthorized', { status: 401 })
}
return user
}
And now we know that if we get the user from
requireUser
they are in fact
logged in! On top of that, you can throw more than just 400s, you could even
throw a redirect!export async function requireUser(request: Request) {
const user = await getUser(request)
if (!user) {
throw new Response(null, { status: 302, headers: { Location: '/login' } })
}
return user
}
Remix has a handy utility for redirects as well:
import { redirect } from '@remix-run/node'
export async function requireUser(request: Request) {
const user = await getUser(request)
if (!user) {
throw redirect('/login')
}
return user
}
This is a great way to make nice utilities that make the regular application
code much easier to write and read:
import {
json,
type ActionFunctionArgs,
type LoaderFunctionArgs,
type MetaFunction,
} from '@remix-run/node'
import { invariantResponse, useIsSubmitting } from '#app/utils/misc.tsx'
import { requireUser } from '#app/utils/auth.server'
import { requireSandwich } from '#app/utils/sandwiches.server'
import { getUser } from '#app/utils/auth.server'
import { getSandwich } from '#app/utils/sandwiches.server'
export async function loader({ request, params }: LoaderFunctionArgs) {
const user = await requireUser(request)
const user = await getUser(request)
if (!user) {
// this response will be handled by our error boundary
throw new Response('Unauthorized', { status: 401 })
}
// this invariant with throw an error which our error boundary will handle as well
invariantResponse(params.sandwichId, 'sandwichId is required')
const sandwich = await requireSandwich(params.sandwichId)
const sandwich = await getSandwich(params.sandwichId)
if (!sandwich) {
// this response will be handled by our error boundary
throw new Response('Not Found', { status: 404 })
}
return json({ sandwich })
}
๐จโ๐ผ Great, with all that knowledge, now I'd like you to upgrade our error
boundary in to handle
a 404. Once you're done, you should be able to go to
and see a nice error message there.