Repository: TanStack/router
Pull request #3037 (2024-12-18 22:42:59)

fix: allow serverFn errors to also have context

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit cc90bf282b5bca1603862494a698c61d738b454d.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/6763500ab8ebfd3142198c38?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/d3bc4c77-7fb8-4b80-9d59-13e688fc6804)
<details><summary><b>More templates</b></summary><p>

- [tanstack-router-react-example-authenticated-routes](https://pkg.pr.new/template/fd93c0f2-6171-4676-bb87-6699d41ecd24)
- [tanstack-router-react-example-basic](https://pkg.pr.new/template/b64040b5-4c33-4e39-930e-f6af018518e5)
- [tanstack-router-react-example-basic-file-based](https://pkg.pr.new/template/404ee58c-6570-46f6-9976-ff3fe8d0876d)
- [tanstack-router-react-example-basic-default-search-params](https://pkg.pr.new/template/99cca25d-2699-4e45-847a-8b3501cb13f4)
- [tanstack-router-react-example-basic-file-based-codesplitting](https://pkg.pr.new/template/e08cb8c1-c19e-4513-afa9-67d294b06841)
- [tanstack-router-react-example-react-query](https://pkg.pr.new/template/5d8340f1-bf3b-4ecb-ba3d-7688a259a807)
- [tanstack-router-react-example-basic-react-query-file-based](https://pkg.pr.new/template/efc1f90d-83d6-48f7-b17f-fd4280041692)
- [tanstack-router-react-example-basic-ssr-streaming-file-based](https://pkg.pr.new/template/f7e27d4b-0892-4514-b969-1ab28028440b)
- [tanstack-router-react-example-basic-ssr-file-based](https://pkg.pr.new/template/cdc3d6c5-c534-49a3-bf00-8ed18029ecf5)
- [tanstack-router-react-example-basic-virtual-file-based](https://pkg.pr.new/template/ed9d5e3c-c038-4fef-88c4-e1b40faf505c)
- [tanstack-router-react-example-basic-virtual-inside-file-based](https://pkg.pr.new/template/7e7a66ef-1a57-40d7-a9ec-89f58553b129)
- [tanstack-router-react-example-deferred-data](https://pkg.pr.new/template/7fcde7ef-04e6-42d9-95fa-93d57183c228)
- [tanstack-router-react-example-kitchen-sink](https://pkg.pr.new/template/c437c917-f7b6-4066-a386-f22da77ef4d6)
- [tanstack-router-react-example-kitchen-sink-file-based](https://pkg.pr.new/template/46cae710-7a07-412f-a601-c1a8cbc7370d)
- [tanstack-router-react-example-kitchen-sink-react-query](https://pkg.pr.new/template/1bd5231b-23e6-4900-822b-95b946a7bdf3)
- [tanstack-router-react-example-kitchen-sink-react-query-file-based](https://pkg.pr.new/template/c56f180d-6dc4-4483-9608-600baa8381fb)
- [tanstack-router-react-example-large-file-based](https://pkg.pr.new/template/f581a0dd-e808-4969-ad0f-43796d567fa4)
- [tanstack-router-react-example-location-masking](https://pkg.pr.new/template/52760bbb-92ab-4f17-9e36-b904a16dabfc)
- [tanstack-router-react-example-navigation-blocking](https://pkg.pr.new/template/d074b50c-f929-47a4-b187-d39b36c6b284)
- [tanstack-router-react-example-quickstart](https://pkg.pr.new/template/174d1615-194c-4d68-8154-f7ca7c8e6018)
- [tanstack-router-react-example-quickstart-esbuild-file-based](https://pkg.pr.new/template/ad3daa71-5ade-4b26-85de-0902e6c0d3a1)
- [tanstack-router-react-example-quickstart-file-based](https://pkg.pr.new/template/8e191b71-9aeb-408e-b350-4d2546ab6546)
- [tanstack-router-react-example-quickstart-webpack-file-based](https://pkg.pr.new/template/bdbc7b22-54a9-454b-9757-df5c3e0d9954)
- [tanstack-router-react-example-quickstart-rspack-file-based](https://pkg.pr.new/template/63fff87b-45ae-4ea7-b807-3d161fb02226)
- [router-monorepo-react-query](https://pkg.pr.new/template/7beabffa-8d29-4bbd-b40e-d67ae7bd6e62)
- [router-mono-simple](https://pkg.pr.new/template/6ce177af-3c5d-4317-b88c-9f8afc222773)
- [tanstack-router-react-example-scroll-restoration](https://pkg.pr.new/template/1a381d46-92c6-4615-9945-83f9b276ceb4)
- [tanstack-search-validator-adapters](https://pkg.pr.new/template/6f5b564b-9791-4386-bf6c-fca8b3619e73)
- [tanstack-start-example-basic](https://pkg.pr.new/template/bbfeca74-8df6-4b4a-a47f-89f0eee64116)
- [tanstack-start-example-basic-auth](https://pkg.pr.new/template/a9027e61-b99b-4590-9ec7-87fc38d20807)
- [tanstack-start-example-basic-react-query](https://pkg.pr.new/template/527f8229-a7cd-43a6-bf5d-98a706585837)
- [tanstack-start-example-basic-rsc](https://pkg.pr.new/template/9db5c7a7-f2ee-472d-81d9-c9f1db706b65)
- [tanstack-start-example-clerk-basic](https://pkg.pr.new/template/144750a4-1646-4f03-ba29-ec23f1f7f511)
- [tanstack-start-example-convex-trellaux](https://pkg.pr.new/template/e6259084-453c-4008-868d-bf351e83eb95)
- [tanstack-start-example-counter](https://pkg.pr.new/template/c0f9e35f-f543-4637-a25c-1185d4fa7255)
- [tanstack-start-example-large](https://pkg.pr.new/template/0ae6dc5f-31ba-4adc-9167-53efdaa12072)
- [tanstack-start-example-trellaux](https://pkg.pr.new/template/b4082216-7be0-4846-87f3-957cc79b7a10)
- [tanstack-start-example-supabase-basic](https://pkg.pr.new/template/ddb6c548-dc0d-46f5-815c-92f899782049)
- [tanstack-router-react-example-with-framer-motion](https://pkg.pr.new/template/12d6fa51-15e2-4043-be2f-cddc548a3621)
- [tanstack-router-react-example-with-trpc](https://pkg.pr.new/template/a59e1adc-bf7c-4cd1-9a14-bdedac84f47a)
- [tanstack-router-react-example-with-trpc-react-query](https://pkg.pr.new/template/79113f0a-5dd2-4ff6-8b02-ce2f7329b162)

</p></details>
      
    


<details><summary><b>@tanstack/eslint-plugin-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/react-cross-context</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-cross-context@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/create-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/create-router@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router-with-query</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router-with-query@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/react-router</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/react-router@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/history</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/history@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/arktype-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/arktype-adapter@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/router-cli</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-cli@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/router-devtools</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-devtools@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/router-generator</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-generator@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/router-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-plugin@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/router-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/router-vite-plugin@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/start</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/start-vite-plugin</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/start-vite-plugin@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/valibot-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/valibot-adapter@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/virtual-file-routes</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/virtual-file-routes@3037
```

</p></details>
      
    

<details><summary><b>@tanstack/zod-adapter</b></summary><p>

```
npm i https://pkg.pr.new/@tanstack/zod-adapter@3037
```

</p></details>
      
    

_commit: <a href="https://github.com/TanStack/router/runs/34624462728"><code>cc90bf2</code></a>_

tannerlinsley: Logs gone.

Patch:
diff --git a/packages/react-router/src/transformer.ts b/packages/react-router/src/transformer.ts
index 7a915aba2..f58f03bb5 100644
--- a/packages/react-router/src/transformer.ts
+++ b/packages/react-router/src/transformer.ts
@@ -3,49 +3,128 @@ import { isPlainObject } from './utils'
 export interface RouterTransformer {
   stringify: (obj: unknown) => string
   parse: (str: string) => unknown
+  encode: <T>(value: T) => T
+  decode: <T>(value: T) => T
 }
 
 export const defaultTransformer: RouterTransformer = {
   stringify: (value: any) =>
-    JSON.stringify(value, function replacer(key, value) {
-      const keyVal = this[key]
-      const transformer = transformers.find((t) => t.stringifyCondition(keyVal))
+    JSON.stringify(value, function replacer(key, val) {
+      const ogVal = this[key]
+      const transformer = transformers.find((t) => t.stringifyCondition(ogVal))
 
       if (transformer) {
-        return transformer.stringify(keyVal)
+        return transformer.stringify(ogVal)
       }
 
-      return value
+      return val
     }),
   parse: (value: string) =>
-    JSON.parse(value, function parser(key, value) {
-      const keyVal = this[key]
-      const transformer = transformers.find((t) => t.parseCondition(keyVal))
+    JSON.parse(value, function parser(key, val) {
+      const ogVal = this[key]
+      if (isPlainObject(ogVal)) {
+        const transformer = transformers.find((t) => t.parseCondition(ogVal))
 
-      if (transformer) {
-        return transformer.parse(keyVal)
+        if (transformer) {
+          return transformer.parse(ogVal)
+        }
       }
 
-      return value
+      return val
     }),
-}
+  encode: (value: any) => {
+    // When encodign, dive first
+    if (Array.isArray(value)) {
+      return value.map((v) => defaultTransformer.encode(v))
+    }
 
-const transformers = [
-  {
-    // Dates
-    stringifyCondition: (value: any) => value instanceof Date,
-    stringify: (value: any) => ({ $date: value.toISOString() }),
-    parseCondition: (value: any) => isPlainObject(value) && value.$date,
-    parse: (value: any) => new Date(value.$date),
+    if (isPlainObject(value)) {
+      return Object.fromEntries(
+        Object.entries(value).map(([key, v]) => [
+          key,
+          defaultTransformer.encode(v),
+        ]),
+      )
+    }
+
+    const transformer = transformers.find((t) => t.stringifyCondition(value))
+    if (transformer) {
+      return transformer.stringify(value)
+    }
+
+    return value
   },
-  {
-    // undefined
-    stringifyCondition: (value: any) => value === undefined,
-    stringify: () => ({ $undefined: '' }),
-    parseCondition: (value: any) =>
-      isPlainObject(value) && value.$undefined === '',
-    parse: () => undefined,
+  decode: (value: any) => {
+    // Attempt transform first
+    if (isPlainObject(value)) {
+      const transformer = transformers.find((t) => t.parseCondition(value))
+      if (transformer) {
+        return transformer.parse(value)
+      }
+    }
+
+    if (Array.isArray(value)) {
+      return value.map((v) => defaultTransformer.decode(v))
+    }
+
+    if (isPlainObject(value)) {
+      return Object.fromEntries(
+        Object.entries(value).map(([key, v]) => [
+          key,
+          defaultTransformer.decode(v),
+        ]),
+      )
+    }
+
+    return value
   },
+}
+
+const createTransformer = <T extends string>(
+  key: T,
+  check: (value: any) => boolean,
+  toValue: (value: any) => any = (v) => v,
+  fromValue: (value: any) => any = (v) => v,
+) => ({
+  key,
+  stringifyCondition: check,
+  stringify: (value: any) => ({ [`$${key}`]: toValue(value) }),
+  parseCondition: (value: any) => Object.hasOwn(value, `$${key}`),
+  parse: (value: any) => fromValue(value[`$${key}`]),
+})
+
+// Keep these ordered by predicted frequency
+const transformers = [
+  createTransformer(
+    // Key
+    'undefined',
+    // Check
+    (v) => v === undefined,
+    // To
+    () => 0,
+    // From
+    () => undefined,
+  ),
+  createTransformer(
+    // Key
+    'date',
+    // Check
+    (v) => v instanceof Date,
+    // To
+    (v) => v.toISOString(),
+    // From
+    (v) => new Date(v),
+  ),
+  createTransformer(
+    // Key
+    'error',
+    // Check
+    (v) => v instanceof Error,
+    // To
+    (v) => ({ ...v, message: v.message, stack: v.stack, cause: v.cause }),
+    // From
+    (v) => Object.assign(new Error(v.message), v),
+  ),
 ] as const
 
 export type TransformerStringify<T, TSerializable> = T extends TSerializable
diff --git a/packages/start/src/client-runtime/fetcher.tsx b/packages/start/src/client-runtime/fetcher.tsx
index 70eea4b9a..e47cc2dd4 100644
--- a/packages/start/src/client-runtime/fetcher.tsx
+++ b/packages/start/src/client-runtime/fetcher.tsx
@@ -59,12 +59,13 @@ export async function fetcher(
 
     // Check if the response is JSON
     if (response.headers.get('content-type')?.includes('application/json')) {
-      const text = await response.text()
-      const json = text ? defaultTransformer.parse(text) : undefined
+      // Even though the response is JSON, we need to decode it
+      // because the server may have transformed it
+      const json = defaultTransformer.decode(await response.json())
 
       // If the response is a redirect or not found, throw it
       // for the router to handle
-      if (isRedirect(json) || isNotFound(json)) {
+      if (isRedirect(json) || isNotFound(json) || json instanceof Error) {
         throw json
       }
 
@@ -90,14 +91,13 @@ export async function fetcher(
 
   // If the response is JSON, return it parsed
   const contentType = response.headers.get('content-type')
-  const text = await response.text()
   if (contentType && contentType.includes('application/json')) {
-    return text ? JSON.parse(text) : undefined
+    return defaultTransformer.decode(await response.json())
   } else {
     // Otherwise, return the text as a fallback
     // If the user wants more than this, they can pass a
     // request instead
-    return text
+    return response.text()
   }
 }
 
@@ -126,27 +126,11 @@ async function handleResponseErrors(response: Response) {
     const contentType = response.headers.get('content-type')
     const isJson = contentType && contentType.includes('application/json')
 
-    const body = await (async () => {
-      if (isJson) {
-        return await response.json()
-      }
-      return await response.text()
-    })()
-
-    const message = `Request failed with status ${response.status}`
-
     if (isJson) {
-      throw new Error(
-        JSON.stringify({
-          message,
-          body,
-        }),
-      )
-    } else {
-      throw new Error(
-        [message, `${JSON.stringify(body, null, 2)}`].join('\n\n'),
-      )
+      throw defaultTransformer.decode(await response.json())
     }
+
+    throw new Error(await response.text())
   }
 
   return response
diff --git a/packages/start/src/client/createMiddleware.ts b/packages/start/src/client/createMiddleware.ts
index 6eefb7751..33d7f7126 100644
--- a/packages/start/src/client/createMiddleware.ts
+++ b/packages/start/src/client/createMiddleware.ts
@@ -124,6 +124,9 @@ export interface MiddlewareServerFnOptions<
   data: Expand<MergeAllValidatorOutputs<TMiddlewares, TValidator>>
   context: Expand<MergeAllServerContext<TMiddlewares, TServerContext>>
   next: MiddlewareServerNextFn
+  method: Method
+  filename: string
+  functionId: string
 }
 
 export type MiddlewareServerFn<
@@ -134,9 +137,11 @@ export type MiddlewareServerFn<
   TNewClientAfterContext,
 > = (
   options: MiddlewareServerFnOptions<TMiddlewares, TValidator, TServerContext>,
-) =>
-  | Promise<ServerResultWithContext<TNewServerContext, TNewClientAfterContext>>
-  | ServerResultWithContext<TNewServerContext, TNewClientAfterContext>
+) => MiddlewareServerFnResult<TNewServerContext, TNewClientAfterContext>
+
+export type MiddlewareServerFnResult<TServerContext, TClientAfterContext> =
+  | Promise<ServerResultWithContext<TServerContext, TClientAfterContext>>
+  | ServerResultWithContext<TServerContext, TClientAfterContext>
 
 export type MiddlewareClientNextFn = <
   TNewServerContext = undefined,
@@ -156,6 +161,8 @@ export interface MiddlewareClientFnOptions<
   sendContext?: unknown // cc Chris Horobin
   method: Method
   next: MiddlewareClientNextFn
+  filename: string
+  functionId: string
 }
 
 export type MiddlewareClientFn<
@@ -165,7 +172,9 @@ export type MiddlewareClientFn<
   TClientContext,
 > = (
   options: MiddlewareClientFnOptions<TMiddlewares, TValidator>,
-) =>
+) => MiddlewareClientFnResult<TServerContext, TClientContext>
+
+export type MiddlewareClientFnResult<TServerContext, TClientContext> =
   | Promise<ClientResultWithContext<TServerContext, TClientContext>>
   | ClientResultWithContext<TServerContext, TClientContext>
 
@@ -208,7 +217,9 @@ export type MiddlewareClientAfterFn<
     TClientContext,
     TClientAfterContext
   >,
-) =>
+) => MiddlewareClientAfterFnResult<TNewClientAfterContext>
+
+export type MiddlewareClientAfterFnResult<TNewClientAfterContext> =
   | Promise<ClientAfterResultWithContext<TNewClientAfterContext>>
   | ClientAfterResultWithContext<TNewClientAfterContext>
 
diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts
index d98522489..5747b402a 100644
--- a/packages/start/src/client/createServerFn.ts
+++ b/packages/start/src/client/createServerFn.ts
@@ -1,5 +1,9 @@
 import invariant from 'tiny-invariant'
-import { defaultTransformer } from '@tanstack/react-router'
+import {
+  defaultTransformer,
+  isNotFound,
+  isRedirect,
+} from '@tanstack/react-router'
 import { mergeHeaders } from './headers'
 import { globalMiddleware } from './registerGlobalMiddleware'
 import type {
@@ -17,6 +21,8 @@ import type {
   MergeAllServerContext,
   MergeAllValidatorInputs,
   MergeAllValidatorOutputs,
+  MiddlewareClientFnResult,
+  MiddlewareServerFnResult,
 } from './createMiddleware'
 
 export interface JsonResponse<TData> extends Response {
@@ -27,6 +33,7 @@ export type CompiledFetcherFnOptions = {
   method: Method
   data: unknown
   headers?: HeadersInit
+  context?: any
 }
 
 export type Fetcher<TMiddlewares, TValidator, TResponse> = {
@@ -35,6 +42,7 @@ export type Fetcher<TMiddlewares, TValidator, TResponse> = {
     method: Method
     data: unknown
     headers?: HeadersInit
+    context?: any
   }) => Promise<unknown>
 } & FetcherImpl<TMiddlewares, TValidator, TResponse>
 
@@ -86,7 +94,9 @@ export interface ServerFnCtx<TMethod, TMiddlewares, TValidator> {
 }
 
 export type CompiledFetcherFn<TResponse> = {
-  (opts: CompiledFetcherFnOptions & ServerFnBaseOptions): Promise<TResponse>
+  (
+    opts: CompiledFetcherFnOptions & ServerFnBaseOptions<Method>,
+  ): Promise<TResponse>
   url: string
 }
 
@@ -240,25 +250,35 @@ export function createServerFn<
             data: opts?.data as any,
             headers: opts?.headers,
             context: Object.assign({}, extractedFn),
-          }).then((d) => d.result)
+          }).then((d) => {
+            if (d.error) throw d.error
+            return d.result
+          })
         },
         {
           // This copies over the URL, function ID and filename
           ...extractedFn,
           // The extracted function on the server-side calls
           // this function
-          __executeServer: (opts: any) => {
+          __executeServer: async (opts: any) => {
             const parsedOpts =
               opts instanceof FormData ? extractFormDataContext(opts) : opts
 
-            return executeMiddleware(resolvedMiddleware, 'server', {
-              ...extractedFn,
-              ...parsedOpts,
-            }).then((d) => ({
+            const result = await executeMiddleware(
+              resolvedMiddleware,
+              'server',
+              {
+                ...extractedFn,
+                ...parsedOpts,
+              },
+            ).then((d) => ({
               // Only send the result and sendContext back to the client
               result: d.result,
+              error: d.error,
               context: d.sendContext,
             }))
+
+            return result
           },
         },
       ) as any
@@ -322,55 +342,43 @@ export type MiddlewareOptions = {
   context?: any
 }
 
-export type MiddlewareResult = {
-  context: any
-  sendContext: any
-  data: any
-  result: unknown
+export type MiddlewareResult = MiddlewareOptions & {
+  result?: unknown
+  error?: unknown
 }
 
-const applyMiddleware = (
-  middlewareFn: NonNullable<
-    | AnyMiddleware['options']['client']
-    | AnyMiddleware['options']['server']
-    | AnyMiddleware['options']['clientAfter']
-  >,
-  mCtx: MiddlewareOptions,
-  nextFn: (ctx: MiddlewareOptions) => Promise<MiddlewareResult>,
-) => {
-  return middlewareFn({
-    data: mCtx.data,
-    context: mCtx.context,
-    sendContext: mCtx.sendContext,
-    method: mCtx.method,
-    next: ((userResult: any) => {
-      // Take the user provided context
-      // and merge it with the current context
-      const context = {
-        ...mCtx.context,
-        ...userResult?.context,
-      }
-
-      const sendContext = {
-        ...mCtx.sendContext,
-        ...(userResult?.sendContext ?? {}),
-      }
+export type NextFn = (ctx: MiddlewareResult) => Promise<MiddlewareResult>
 
-      const headers = mergeHeaders(mCtx.headers, userResult?.headers)
+export type MiddlewareFn = (
+  ctx: MiddlewareOptions & {
+    next: NextFn
+  },
+) => Promise<MiddlewareResult>
 
+const applyMiddleware = async (
+  middlewareFn: MiddlewareFn,
+  ctx: MiddlewareOptions,
+  nextFn: NextFn,
+) => {
+  return middlewareFn({
+    ...ctx,
+    next: (async (userCtx: MiddlewareResult | undefined = {} as any) => {
       // Return the next middleware
       return nextFn({
-        method: mCtx.method,
-        data: mCtx.data,
-        context,
-        sendContext,
-        headers,
+        ...ctx,
+        ...userCtx,
+        context: {
+          ...ctx.context,
+          ...userCtx.context,
+        },
+        sendContext: {
+          ...ctx.sendContext,
+          ...(userCtx.sendContext ?? {}),
+        },
+        headers: mergeHeaders(ctx.headers, userCtx.headers),
         result:
-          userResult?.result !== undefined
-            ? userResult.result
-            : (mCtx as any).result,
-      } as MiddlewareResult & {
-        method: Method
+          userCtx.result !== undefined ? userCtx.result : (ctx as any).result,
+        error: userCtx.error ?? (ctx as any).error,
       })
     }) as any,
   })
@@ -412,13 +420,13 @@ async function executeMiddleware(
     ...middlewares,
   ])
 
-  const next = async (ctx: MiddlewareOptions): Promise<MiddlewareResult> => {
+  const next: NextFn = async (ctx) => {
     // Get the next middleware
     const nextMiddleware = flattenedMiddlewares.shift()
 
     // If there are no more middlewares, return the context
     if (!nextMiddleware) {
-      return ctx as any
+      return ctx
     }
 
     if (
@@ -429,33 +437,47 @@ async function executeMiddleware(
       ctx.data = await execValidator(nextMiddleware.options.validator, ctx.data)
     }
 
-    const middlewareFn =
+    const middlewareFn = (
       env === 'client'
         ? nextMiddleware.options.client
         : nextMiddleware.options.server
+    ) as MiddlewareFn | undefined
 
     if (middlewareFn) {
       // Execute the middleware
-      return applyMiddleware(
-        middlewareFn,
-        ctx,
-        async (userCtx): Promise<MiddlewareResult> => {
-          // If there is a clientAfter function and we are on the client
-          if (env === 'client' && nextMiddleware.options.clientAfter) {
-            // We need to await the next middleware and get the result
-            const result = await next(userCtx)
-            // Then we can execute the clientAfter function
-            return applyMiddleware(
-              nextMiddleware.options.clientAfter,
-              result as any,
-              // Identity, because there "next" is just returning
-              (d: any) => d,
-            ) as any
+      return applyMiddleware(middlewareFn, ctx, async (newCtx) => {
+        // If there is a clientAfter function and we are on the client
+        const clientAfter = nextMiddleware.options.clientAfter as
+          | MiddlewareFn
+          | undefined
+
+        if (env === 'client' && clientAfter) {
+          // We need to await the next middleware and get the result
+          const result = await next(newCtx)
+
+          // Then we can execute the clientAfter function
+          return applyMiddleware(
+            clientAfter,
+            {
+              ...newCtx,
+              ...result,
+            },
+            // Identity, because there "next" is just returning
+            (d: any) => d,
+          )
+        }
+
+        return next(newCtx).catch((error) => {
+          if (isRedirect(error) || isNotFound(error)) {
+            return {
+              ...newCtx,
+              error,
+            }
           }
 
-          return next(userCtx)
-        },
-      ) as any
+          throw error
+        })
+      })
     }
 
     return next(ctx)
@@ -465,7 +487,7 @@ async function executeMiddleware(
   return next({
     ...opts,
     headers: opts.headers || {},
-    sendContext: (opts as any).sendContext || {},
+    sendContext: opts.sendContext || {},
     context: opts.context || {},
   })
 }
@@ -481,21 +503,22 @@ function serverFnBaseToMiddleware(
       client: async ({ next, sendContext, ...ctx }) => {
         // Execute the extracted function
         // but not before serializing the context
-        const res = await options.extractedFn?.({
+        const serverCtx = await options.extractedFn?.({
           ...ctx,
           // switch the sendContext over to context
           context: sendContext,
-        } as any)
+        })
 
-        return next(res)
+        return next(serverCtx) as unknown as MiddlewareClientFnResult<any, any>
       },
       server: async ({ next, ...ctx }) => {
         // Execute the server function
-        const result = await options.serverFn?.(ctx as any)
+        const result = await options.serverFn?.(ctx)
 
         return next({
+          ...ctx,
           result,
-        } as any)
+        } as any) as unknown as MiddlewareServerFnResult<any, any>
       },
     },
   }
diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx
index 6c28b2536..59b4b67a9 100644
--- a/packages/start/src/server-handler/index.tsx
+++ b/packages/start/src/server-handler/index.tsx
@@ -147,7 +147,7 @@ export async function handleServerRequest(request: Request, _event?: H3Event) {
       console.error(error)
       console.info()
 
-      return new Response(JSON.stringify(error), {
+      return new Response(defaultTransformer.stringify(error), {
         status: 500,
         headers: {
           'Content-Type': 'application/json',


Test patch:
diff --git a/packages/react-router/tests/transformer.test.tsx b/packages/react-router/tests/transformer.test.tsx
index cf7e6c67a..f0278b658 100644
--- a/packages/react-router/tests/transformer.test.tsx
+++ b/packages/react-router/tests/transformer.test.tsx
@@ -11,9 +11,9 @@ describe('transformer.stringify', () => {
   })
 
   test('should stringify undefined', () => {
-    expect(defaultTransformer.stringify(undefined)).toMatchInlineSnapshot(`
-      "{"$undefined":""}"
-    `)
+    expect(defaultTransformer.stringify(undefined)).toMatchInlineSnapshot(
+      `"{"$undefined":0}"`,
+    )
   })
 
   test('should stringify object foo="bar"', () => {
@@ -23,10 +23,9 @@ describe('transformer.stringify', () => {
   })
 
   test('should stringify object foo=undefined', () => {
-    expect(defaultTransformer.stringify({ foo: undefined }))
-      .toMatchInlineSnapshot(`
-      "{"foo":{"$undefined":""}}"
-    `)
+    expect(
+      defaultTransformer.stringify({ foo: undefined }),
+    ).toMatchInlineSnapshot(`"{"foo":{"$undefined":0}}"`)
   })
 
   test('should stringify object foo=Date', () => {