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

  1. Call the full path down to a leaf action: actions.blog.like() instead of actions.blog()
  2. Remove or move non-action exports (constants, helper objects) out of actions/index.ts so the namespace only contains handlers
  3. 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

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


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)