withastro/astro · error · ActionError

INTERNAL_SERVER_ERROR

INTERNAL_SERVER_ERROR

Error message

e instanceof Error ? e.message : 'Unknown error'

What it means

When an action handler throws anything that is not an `ActionError` (or `ActionInputError`), the runtime wraps it in a generic `ActionError` with `code: 'INTERNAL_SERVER_ERROR'`, preserving the original `e.message` (or 'Unknown error' for non-Error throws). This is the catch-all for unexpected bugs inside your action handler code.

Source

Thrown at packages/astro/src/actions/runtime/server.ts:463

	}
	return schema;
}

async function callSafely<TOutput>(
	handler: () => MaybePromise<TOutput>,
): Promise<SafeResult<z.$ZodType, TOutput>> {
	try {
		const data = await handler();
		return { data, error: undefined };
	} catch (e) {
		if (e instanceof ActionError) {
			return { data: undefined, error: e };
		}
		return {
			data: undefined,
			error: new ActionError({
				message: e instanceof Error ? e.message : 'Unknown error',
				code: 'INTERNAL_SERVER_ERROR',
			}),
		};
	}
}

export function serializeActionResult(res: SafeResult<any, any>): SerializedActionResult {
	if (res.error) {
		if (import.meta.env?.DEV) {
			actionResultErrorStack.set(res.error.stack);
		}

		let body: Record<string, any>;
		if (res.error instanceof ActionInputError) {
			body = {
				type: res.error.type,
				issues: res.error.issues,
				fields: res.error.fields,
			};

View on GitHub (pinned to 52e6c34790)

Solutions

  1. Read the preserved message — it is the original error's text from inside your handler
  2. Reproduce the handler logic directly (outside the action) or log at the throw site to get a stack
  3. Convert expected failures into ActionError with a proper code so clients branch on it
  4. Check env vars/services the handler depends on (DB URL, API keys) in the failing environment

Example fix

// before
handler: async (input) => {
  const user = await db.user.findFirstOrThrow({ where: { id: input.id } }); // throws raw
},

// after
import { defineAction, ActionError } from 'astro:actions';
handler: async (input) => {
  const user = await db.user.findFirst({ where: { id: input.id } });
  if (!user) throw new ActionError({ code: 'NOT_FOUND', message: 'User not found' });
  return user;
},
Defensive patterns

Strategy: try-catch

Type guard

function isActionError(e: unknown): e is ActionError {
  return e instanceof ActionError;
}

Try / catch

const result = await actions.user.update.safe(input);
if (result.error) {
  switch (result.error.code) {
    case 'INTERNAL_SERVER_ERROR':
      // original bug inside the handler; log result.error.message and alert
      break;
    default:
      throw result.error;
  }
}

Prevention

When it happens

Trigger: Any exception in the handler body: DB connection failures, `undefined is not a function` typos, failed `await context.locals` calls, third-party SDK errors, or throwing a plain string/object instead of an Error.

Common situations: Bugs in newly written handler logic; environment differences (missing env vars in prod); unhandled promise rejections from awaits inside the handler. The wrapper keeps the server from leaking stacks while still surfacing the message.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/2655d3052aca383a. Report an issue: GitHub.