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
- Check the value passed to map/action/addRoute is a function or { handler: fn }
- Fix circular imports or missing exports that make the handler undefined
- If passing an object, ensure the property is named `handler` and is a function
- 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
- Type route maps as RouteMap and controllers as Controller so TS rejects bad shapes
- Run router.map wiring during app startup so misconfiguration fails fast
- Avoid circular imports between route modules and handler modules
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
- Expected a controller with an object `actions` property
- ${optionPath} values must be strings
- hmr must create a channel with a close function
- hmr must create a channel with an onFileEvents function
- hmr must create a channel with an updateWatchedFiles functio
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/7c7579f6ee427f7e.
Report an issue: GitHub.