withastro/astro · error · AstroError
ActionNotFoundError
ActionNotFoundError
Error message
The server received a request for an action named `${actionName}` but could not find a match. If you renamed an action, check that you've updated your `actions/index` file and your calling code to match. What it means
getAction() walks the dot-separated action path through the `server` actions object. Once an intermediate segment resolves to a function, that function is a leaf action: its own properties are not part of the action namespace. Requesting anything deeper than a leaf (e.g. `logout.fetch`) makes traversal step into a function's internals, which Astro rejects with ActionNotFoundError instead of leaking function properties.
Solutions
- Compare the requested dotted path against your actions/index.ts exports and remove the extra trailing segments
- If you intended nesting, wrap the leaf in an object namespace: move the action into a sub-object (folder or nested export) so the deeper key exists
- Bust stale client bundles: hard-reload / rebuild after renaming actions
Example fix
// before — action is a leaf, caller adds a sub-key
export const logout = defineAction({ handler: () => 'ok' });
await actions['logout.session'](); // throws
// after
await actions.logout(); Defensive patterns
Strategy: validation
Validate before calling
// centralize action names and validate against them before calling
const ACTION_PATHS = ['blog.like', 'blog.share', 'logout'] as const;
function isKnownActionPath(p: string): boolean {
return (ACTION_PATHS as readonly string[]).includes(p);
}
if (!isKnownActionPath(name)) return new Response('Unknown action', { status: 404 });
await actions[name as keyof typeof actions](input); Type guard
type ActionPath = 'blog.like' | 'blog.share' | 'logout';
function isActionPath(p: string): p is ActionPath {
return ['blog.like', 'blog.share', 'logout'].includes(p);
} Try / catch
try {
await actions[name](input);
} catch (e) {
if (e instanceof AstroError && e.code === 'ActionNotFoundError') {
return new Response('Unknown action', { status: 404 });
}
throw e;
} Prevention
- Use static dotted access (actions.blog.like()) so the compiler verifies every path segment
- Never build action names from unvalidated user input
- After renaming actions, grep for old names and redeploy client bundles in lockstep with the server
When it happens
Trigger: A client or caller requests an action name with extra segments after a leaf action, e.g. POSTing actionName 'logout.clear' when 'logout' is already a single defineAction. Also stitching paths dynamically where a leaf name is accidentally prefixed to a sub-key, or double-joining with dots ('blog.like' + '.' + 'like').
Common situations: Renaming a nested folder action to a flat one while stale client code still calls the old nested path; typos adding a suffix after an action name; hand-built action-name strings in middleware or custom fetch wrappers.
Related errors
- Expected handler for action
- ActionCalledFromServerError
- ActionCalledFromServerError
- ActionsReturnedInvalidDataError
- [astro:actions] `defineAction()` unexpectedly used on the…
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/904270df93fad83f.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/actions/load.ts:45
/** Looks up a single action handler by its dot-separated path. */
export async function getAction(
manifest: SSRManifest,
path: string,
): Promise<ActionClient<unknown, ActionAccept, $ZodType>> {
const pathKeys = path.split('.').map((key) => decodeURIComponent(key));
let { server } = await getActions(manifest);
if (!server || !(typeof server === 'object')) {
throw new TypeError(
`Expected \`server\` export in actions file to be an object. Received ${typeof server}.`,
);
}
for (const key of pathKeys) {
// An action is a leaf: once resolved to a function, its own properties
// are not part of the action namespace and cannot be traversed further.
if (typeof server === 'function') {
throw new AstroError({
...ActionNotFoundError,
message: ActionNotFoundError.message(pathKeys.join('.')),
});
}
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];View on GitHub (pinned to 52e6c34790)