{"record":{"id":"f91bf1d354f66232","repo":"remix-run/remix","slug":"expected-a-controller-with-an-object-actions-pro","errorCode":null,"errorMessage":"Expected a controller with an object `actions` property","messagePattern":"Expected a controller with an object `actions` property","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/fetch-router/src/lib/router.ts","lineNumber":409,"sourceCode":"  }\n\n  function addRoute(\n    method: RequestMethod | 'ANY',\n    route: RouteTarget,\n    action: unknown,\n    state: BuilderState,\n  ): void {\n    registerRoute(method, route, normalizeAction(action), state)\n  }\n\n  function mapRoutes(target: MapTarget, handler: unknown, state: BuilderState): void {\n    if (isRouteTarget(target)) {\n      mapSingleRoute(target, handler, state)\n      return\n    }\n\n    if (!isController(handler)) {\n      throw new TypeError('Expected a controller with an object `actions` property')\n    }\n\n    mapController(target, handler, state)\n  }\n\n  function mapSingleRoute(target: RouteTarget, handler: unknown, state: BuilderState): void {\n    registerRoute(getMappedRouteMethod(target), target, normalizeAction(handler), state)\n  }\n\n  function mapController(\n    routes: RouteMap,\n    controller: {\n      middleware?: readonly AnyMiddleware[] | undefined\n      actions: Record<string, unknown>\n    },\n    state: BuilderState,\n  ): void {\n    let controllerMiddleware = normalizeMiddleware(controller.middleware)","sourceCodeStart":391,"sourceCodeEnd":427,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/fetch-router/src/lib/router.ts#L391-L427","documentation":"Thrown by mapRoutes in @remix-run/fetch-router when router.map() receives a routes object but the corresponding handler is not a controller, i.e. not an object with an `actions` property. When mapping multiple routes at once, fetch-router expects a controller whose actions object keys match the routes keys; single-route targets accept plain handlers.","triggerScenarios":"router.map({ '/a': ..., '/b': ... }, plainFunction) or router.map(routesObject, { notActions: {...} }). Any multi-key routes target paired with a non-controller handler.","commonSituations":"Passing a single handler function for a routes map, typoing `actions` (e.g. `action` or `handlers`), or passing a controller instance from a different library/version whose shape changed.","solutions":["If mapping one route, pass a single Route target (e.g. route(...)) so mapSingleRoute runs","If mapping multiple routes, wrap handlers in { actions: { key: handler } }","Check the `actions` property spelling and that it is a plain object","Verify controller shape matches the version of fetch-router you depend on"],"exampleFix":"// before\nrouter.map({ '/a': route(get, '/a'), '/b': route(post, '/b') }, handleAll)\n\n// after\nrouter.map(\n  { '/a': route(get, '/a'), '/b': route(post, '/b') },\n  { actions: { '/a': handleA, '/b': handleB } },\n)","handlingStrategy":"type-guard","validationCode":"let isControllerLike = (v: unknown) =>\n  typeof v === 'object' && v !== null && typeof (v as any).actions === 'object'","typeGuard":"function isController(value: unknown): value is { actions: Record<string, unknown>; middleware?: unknown } {\n  return typeof value === 'object' && value !== null && typeof (value as { actions?: unknown }).actions === 'object'\n}","tryCatchPattern":"catch (error) { if (error instanceof TypeError && /Expected a controller/.test(error.message)) throw new Error(`Bad router.map() call: ${error.message}`) throw error }","preventionTips":["Pass single Route targets when mapping one route, controllers when mapping a map","Let TypeScript infer/check the map-vs-handler pairing at compile time","Smoke-test route registration in CI"],"tags":["fetch-router","route-mapping","controller","validation"],"backgroundTag":"invalid-argument-type","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}