remix-run/remix · error · TypeError

Expected a request handler function or action object with a

Error message

Expected a request handler function or action object with a function `handler` property

What it means

Thrown by normalizeAction in @remix-run/fetch-router when the value passed as a route action is neither a function nor an object with a function `handler` property. The router accepts handler functions directly or action objects (with optional middleware), and anything else fails this check. It almost always indicates a type mismatch at the call site, e.g. passing a string, undefined, or an object whose handler is not a function.

Source

Thrown at packages/fetch-router/src/lib/router.ts:265

  return new Response(`Not Found: ${url.pathname}`, { status: 404 })
}

function normalizeMiddleware(
  middleware: readonly AnyMiddleware[] | undefined,
): AnyMiddleware[] | undefined {
  return middleware == null || middleware.length === 0 ? undefined : [...middleware]
}

function normalizeAction(action: unknown): NormalizedAction {
  if (isRequestHandler(action)) {
    return {
      handler: action,
      middleware: undefined,
    }
  }

  if (!isActionObject(action)) {
    throw new TypeError(
      'Expected a request handler function or action object with a function `handler` property',
    )
  }

  return {
    handler: action.handler,
    middleware: normalizeMiddleware(action.middleware),
  }
}

function mergeMiddleware(
  upstream: AnyMiddleware[] | undefined,
  downstream: AnyMiddleware[] | undefined,
): AnyMiddleware[] | undefined {
  if (!upstream || upstream.length === 0) {
    return downstream
  }

View on GitHub (pinned to 9696913134)

Solutions

  1. Check the value passed to map/action/addRoute is a function or { handler: fn }
  2. Fix circular imports or missing exports that make the handler undefined
  3. If passing an object, ensure the property is named `handler` and is a function
  4. If you meant to map a controller, pass it to map() with a routes object so mapController runs instead

Example fix

// before
router.action('/users', UserHandler) // UserHandler is undefined or a string

// after
import { UserHandler } from './handlers.ts'
router.action('/users', { handler: UserHandler })
Defensive patterns

Strategy: type-guard

Validate before calling

let isAction = (v: unknown): boolean =>
  typeof v === 'function' ||
  (typeof v === 'object' && v !== null && typeof (v as any).handler === 'function')

Type guard

function isActionInput(value: unknown): value is Function | { handler: Function } {
  return (
    typeof value === 'function' ||
    (typeof value === 'object' &&
      value !== null &&
      typeof (value as { handler?: unknown }).handler === 'function')
  )
}

Try / catch

catch (error) { if (error instanceof TypeError && /Expected a request handler/.test(error.message)) { /* fix wiring at build time */ } throw error }

Prevention

When it happens

Trigger: Calling router.map(), addRoute(), or router.action() with a non-function value: a string path, undefined (e.g. an import that resolved to undefined), an object without a `handler` key, or handler: 'notAFunction'.

Common situations: Circular imports making the handler undefined, forgetting to call a factory (passing makeHandler instead of makeHandler()), typoing the `handler` property name, or passing a controller object where a single action is expected.

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/7c7579f6ee427f7e. Report an issue: GitHub.