Handle Thrown Responses

🦉 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.

Access Denied

You must login or register for the workshop to view the diff.

Check out this video to see how the diff tab works.