{"record":{"id":"9a97d7d9733c23d8","repo":"withastro/astro","slug":"expected-handler-for-action-pathkeys-join","errorCode":null,"errorMessage":"Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}.","messagePattern":"Expected handler for action (.+?) to be a function\\. Received (.+?)\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/astro/src/actions/load.ts","lineNumber":66,"sourceCode":"\t\t\t});\n\t\t}\n\t\tif (FORBIDDEN_PATH_KEYS.has(key)) {\n\t\t\tthrow new AstroError({\n\t\t\t\t...ActionNotFoundError,\n\t\t\t\tmessage: ActionNotFoundError.message(pathKeys.join('.')),\n\t\t\t});\n\t\t}\n\t\tif (!Object.hasOwn(server, key)) {\n\t\t\tthrow new AstroError({\n\t\t\t\t...ActionNotFoundError,\n\t\t\t\tmessage: ActionNotFoundError.message(pathKeys.join('.')),\n\t\t\t});\n\t\t}\n\t\t// @ts-expect-error we are doing a recursion... it's ugly\n\t\tserver = server[key];\n\t}\n\tif (typeof server !== 'function') {\n\t\tthrow new TypeError(\n\t\t\t`Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}.`,\n\t\t);\n\t}\n\treturn server;\n}\n","sourceCodeStart":48,"sourceCodeEnd":72,"githubUrl":"https://github.com/withastro/astro/blob/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/astro/src/actions/load.ts#L48-L72","documentation":"After getAction() finishes walking the dotted path, the resolved value must be a callable handler. If the path stops on a namespace object — or on any non-function export inside the actions file — traversal 'succeeds' but there is nothing to invoke, so Astro throws this TypeError naming the path and the received type (usually 'object').","triggerScenarios":"Requesting a namespace instead of a leaf action, e.g. POSTing actionName 'blog' when 'blog' is a folder/group object. Exporting a plain object or value (not wrapped in defineAction) inside actions/index.ts, then calling it like an action. Calling a sub-object that groups actions without going one level deeper.","commonSituations":"Structuring actions in nested folders and calling the folder name by mistake; exporting helper objects/constants from actions/index.ts which pollute the namespace; assuming a group callable exists after refactoring flat actions into folders.","solutions":["Call the full path down to a leaf action: actions.blog.like() instead of actions.blog()","Remove or move non-action exports (constants, helper objects) out of actions/index.ts so the namespace only contains handlers","If you expected the target to be an action, wrap it in defineAction()"],"exampleFix":"// before — src/actions/index.ts\nexport const blog = { like: defineAction({ handler: () => 1 }) };\nawait actions.blog(); // resolves to object -> TypeError\n\n// after\nawait actions.blog.like();","handlingStrategy":"type-guard","validationCode":"// before a dynamic call, confirm the resolved export is a handler\nimport * as ns from './actions/index.js';\nconst target = path.split('.').reduce<unknown>((acc, k) => (acc as any)?.[k], ns);\nif (typeof target !== 'function') {\n  throw new Error(`'${path}' is a namespace, not an action. Call a leaf action.`);\n}","typeGuard":"function isActionHandler(v: unknown): v is (input: unknown) => Promise<unknown> {\n  return typeof v === 'function' && 'orThrow' in v;\n}","tryCatchPattern":"try {\n  await actions.blog(input);\n} catch (e) {\n  if (e instanceof TypeError && e.message.startsWith('Expected handler for action')) {\n    // you called a namespace: descend to a leaf action like actions.blog.like\n  } else throw e;\n}","preventionTips":["Export only defineAction() results and nested objects of them from actions files — no constants or helpers","Use TypeScript: typed action access flags namespace-vs-call mistakes at compile time","When organizing actions into folders, always call the full dotted path to a leaf"],"tags":["astro-actions","typeerror","namespace","rpc-lookup"],"backgroundTag":"rpc-method-not-callable","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}