withastro/astro · error · TypeError

Expected handler for action ${pathKeys.join('.')} to be a fu

Error message

Expected handler for action ${pathKeys.join('.')} to be a function. Received ${typeof server}.

What it means

A `TypeError` thrown after the full action path is traversed but the final resolved value is not a function. This means the dotted path points to a valid property on the `server` object, but it's a value (object, string, number) rather than an action handler function. The guard `if (typeof server !== 'function')` fires after the loop.

Source

Thrown at packages/astro/src/core/base-pipeline.ts:401

				});
			}
			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;
	}

	async getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule> {
		for (const defaultRoute of this.defaultRoutes) {
			if (route.component === defaultRoute.component) {
				return {
					page: () => Promise.resolve(defaultRoute.instance),
				};
			}
		}

		if (route.type === 'redirect') {
			return RedirectSinglePageBuiltModule;
		} else {

View on GitHub (pinned to d081033d5f)

Solutions

  1. Check that the path in the error points to an actual `defineAction` handler, not a nested object.
  2. Ensure every leaf in the `server` export is a function (action handler).
  3. Move non-action utilities out of the `server` export.
  4. Verify the action path in `Astro.callAction` ends at a handler function.

Example fix

// before — src/actions/index.ts
export const server = {
  config: { theme: 'dark' }, // not an action
};
// calling Astro.callAction('config.theme') → TypeError

// after
export const server = {
  getTheme: defineAction({
    handler: () => 'dark',
  }),
};
Defensive patterns

Strategy: type-guard

Validate before calling

import { server } from './src/actions/index';
function isActionFunction(server: unknown, path: string): boolean {
  let current: unknown = server;
  for (const key of path.split('.')) {
    if (typeof current !== 'object' || current === null) return false;
    current = (current as Record<string, unknown>)[key];
  }
  return typeof current === 'function';
}

Type guard

function isActionHandler(value: unknown): value is (...args: any[]) => any {
  return typeof value === 'function';
}

Prevention

When it happens

Trigger: `getAction('settings.theme')` is called. Traversal succeeds for all keys, but `server['settings']['theme']` is a string or object, not a function. The final `typeof server !== 'function'` check throws a TypeError naming the path and the actual type received.

Common situations: An action path points to a nested configuration object instead of a handler. A non-action property was accidentally added to the `server` export. The `server` object has helper utilities mixed in with actions. A namespace object is referenced as if it were an action.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/b2e535c83bd69a21. Report an issue: GitHub.