# Throwing vs. Returning Redirects in React Router

One of the most common questions in React Router is: what’s the difference between **throwing** and **returning** a redirect in a loader or action?

## Understanding the `redirect` Function

The `redirect` function in React Router is often misunderstood. Many assume it triggers a side effect that immediately causes navigation. However, it is actually a **pure function** that returns a new `Response` instance.

```ts
export const redirect: RedirectFunction = (url, init = 302) => {
  let responseInit = init;
  if (typeof responseInit === "number") {
    responseInit = { status: responseInit };
  } else if (typeof responseInit.status === "undefined") {
    responseInit.status = 302;
  }

  let headers = new Headers(responseInit.headers);
  headers.set("Location", url);

  return new Response(null, { ...responseInit, headers });
};
```

This code is taken directly from React Router’s source. As you can see, it creates a new `Response` instance with the `Location` header set to the target URL and a default status of `302` (Found).

Because `redirect()` simply returns a `Response`, calling `redirect("/login")` **alone** is not enough to trigger navigation. You must **return** the response from your action or loader so that the router processes it.

```ts
export async function action({ request }: Route.ActionArgs) {
  return redirect("/login");
}
```

## Throwing a Redirect

React Router also allows you to **throw** a redirect instead of returning it.

```ts
export async function action({ request }: Route.ActionArgs) {
  throw redirect("/login");
}
```

When you throw a redirect, React Router **automatically catches it** and processes the response, making it equivalent to returning a redirect in many cases.

## When Does Throwing Matter?

While both returning and throwing a redirect achieve the same result in most cases, there’s a key difference: **throwing stops execution not just in the current function but also in its entire call stack until a `try/catch` block handles it**.

Consider a loader that checks if a user is authenticated and redirects them to the login page if they are not.

```ts
export async function loader({ request }: Route.LoaderArgs) {
  let user = await currentUser(request);
  if (!user) return redirect("/login");
  // ...
}
```

This works as expected: if the user is not authenticated, the loader returns a redirect response, and React Router navigates to the login page.

But what if we want a **helper function** that enforces authentication?

### Returning a Redirect in a Helper Function

```ts
async function requireUser(request: Request, path = "/login") {
  let user = await currentUser(request);
  if (!user) return redirect(path);
  return user;
}

export async function loader({ request }: Route.LoaderArgs) {
  let userOrResponse = await requireUser(request);
  if (userOrResponse instanceof Response) return userOrResponse;
  let user = userOrResponse;
  // ...
}
```

Here, `requireUser()` checks if the user is authenticated. If not, it returns a redirect response, which the loader must check and return explicitly.

### Throwing a Redirect in a Helper Function

Now, let’s modify `requireUser()` to **throw** the redirect instead:

```ts
async function requireUser(request: Request, path = "/login") {
  let user = await currentUser(request);
  if (!user) throw redirect(path);
  return user;
}

export async function loader({ request }: Route.LoaderArgs) {
  let user = await requireUser(request);
  // ...
}
```

With this approach:
- If the user is authenticated, `requireUser()` returns the user object.
- If the user is not authenticated, `requireUser()` **throws** a redirect.
- The router catches the thrown redirect and processes it **without requiring the loader to handle it explicitly**.

This pattern allows you to build **reusable authentication helpers** that automatically redirect users when needed, simplifying your loader and action functions.

## Handling Redirects Inside a `try/catch`

A special case arises when you need to wrap your logic inside a `try/catch` block:

```ts
export async function loader({ request }: Route.LoaderArgs) {
  try {
    let user = await requireUser(request);
    // ...
  } catch (exception) {
    // ...
  }
}
```

In this case, if `requireUser()` throws a redirect, the `catch` block will catch it **before the router does**, preventing the navigation.

To ensure the router still processes the redirect, you should **rethrow** the exception:

```ts
export async function loader({ request }: Route.LoaderArgs) {
  try {
    let user = await requireUser(request);
    // ...
  } catch (exception) {
    if (exception instanceof Response) throw exception;
    // ...
  }
}
```

This way:
- If `requireUser()` throws a redirect, the `catch` block rethrows it, allowing React Router to handle the navigation.
- If `requireUser()` throws any other type of error, you can handle it separately.

## Summary

- The `redirect()` function **returns** a `Response`, which must be returned from a loader/action to trigger navigation.
- You can **throw** a redirect, and React Router will catch it and handle it automatically.
- Throwing stops execution at all levels, making it useful for enforcing authentication with helper functions.
- If you use a `try/catch` block, ensure you **rethrow redirects** so that React Router can still process them.

By understanding these differences, you can write cleaner, more efficient loaders and actions in your React Router applications.
