Skip to content

Middleware error handling: returning custom values from catch blocks doesn't reach the client #7238

@alexander-zuev

Description

@alexander-zuev

Problem

We use function middleware to catch errors and convert them to structured error responses so the client can branch on error codes (show field-level validation errors, redirect on auth failures, show billing-specific messages, etc.).

export const errorHandler = createMiddleware({ type: 'function' }).server(async ({ next }) => {
  try {
    return await next()
  } catch (error) {
    if (error instanceof DomainError) {
      return { success: false, error: { code: 'BILLING_ERROR', message: error.message } }
    }
    return { success: false, error: { code: 'INTERNAL_SERVER_ERROR', message: 'Something went wrong' } }
  }
})

Registered globally:

createStart(() => ({
  functionMiddleware: [errorHandler],
}))

Expected: Client receives { success: false, error: { code: 'BILLING_ERROR', ... } } as the resolved value.

Actual: Client promise rejects with just the inner error object { code: 'BILLING_ERROR', ... }. The { success: false } wrapper is stripped.

Root cause

executeMiddleware uses { result, error } internally (ServerFnMiddlewareResult). When middleware returns an object with an .error property, line 140 of createServerFn.ts treats it as a middleware-chain error and throws it:

if (result.error) throw result.error

Any return value from middleware that has a truthy .error field collides with this internal protocol. The middleware's return is never delivered to the client as a resolved value.

What we need

We want a global error boundary for server functions — similar to FastAPI exception handlers or Express error middleware. The goal is:

  1. Classified errors (auth, billing, validation) — send a structured error code to the client so it can react (redirect, show field errors, show billing prompts)
  2. Unexpected errors — log server-side for debugging, send a generic "Something went wrong" to the client
  3. Single boundary — handlers just throw, middleware handles classification

Questions

  1. Is returning custom values from middleware catch blocks intended to work? The docs only show return next() or throw from middleware — never custom returns.
  2. If not, what's the recommended pattern for global error classification in server functions? Should middleware throw a structured error object and let the client catch the rejection?
  3. Would TanStack consider documenting an official error-handling middleware pattern?

Environment

  • @tanstack/react-start v1.167.17
  • @tanstack/start-client-core v1.167.17

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions