withastro/astro · error · TypeError
Expected handler for action
Error message
Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}. What it means
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').
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()
Example fix
// before — src/actions/index.ts
export const blog = { like: defineAction({ handler: () => 1 }) };
await actions.blog(); // resolves to object -> TypeError
// after
await actions.blog.like(); Defensive patterns
Strategy: type-guard
Validate before calling
// before a dynamic call, confirm the resolved export is a handler
import * as ns from './actions/index.js';
const target = path.split('.').reduce<unknown>((acc, k) => (acc as any)?.[k], ns);
if (typeof target !== 'function') {
throw new Error(`'${path}' is a namespace, not an action. Call a leaf action.`);
} Type guard
function isActionHandler(v: unknown): v is (input: unknown) => Promise<unknown> {
return typeof v === 'function' && 'orThrow' in v;
} Try / catch
try {
await actions.blog(input);
} catch (e) {
if (e instanceof TypeError && e.message.startsWith('Expected handler for action')) {
// you called a namespace: descend to a leaf action like actions.blog.like
} else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ActionNotFoundError
- Expected `server` export in actions file to be an object…
- Unsupported content type
- ActionCalledFromServerError
- ActionCalledFromServerError
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/9a97d7d9733c23d8.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/actions/load.ts:66
});
}
if (FORBIDDEN_PATH_KEYS.has(key)) {
throw new AstroError({
...ActionNotFoundError,
message: ActionNotFoundError.message(pathKeys.join('.')),
});
}
if (!Object.hasOwn(server, key)) {
throw new AstroError({
...ActionNotFoundError,
message: ActionNotFoundError.message(pathKeys.join('.')),
});
}
// @ts-expect-error we are doing a recursion... it's ugly
server = server[key];
}
if (typeof server !== 'function') {
throw new TypeError(
`Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}.`,
);
}
return server;
}
View on GitHub (pinned to 52e6c34790)