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
- Read the preserved message — it is the original error's text from inside your handler
- Reproduce the handler logic directly (outside the action) or log at the throw site to get a stack
- Convert expected failures into ActionError with a proper code so clients branch on it
- 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
- Use the .safe() variant to get typed errors instead of exceptions in callers
- Throw ActionError with precise codes for expected failures; reserve INTERNAL_SERVER_ERROR for true bugs
- Log inside handlers around risky awaits (DB, fetch) to capture stacks the wrapper hides
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
- Request body exceeds the configured limit of ${limit} bytes
- `getActionState()` must be called with a form request.
- `getActionState()` could not find a state object.
- BAD_REQUEST
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/2655d3052aca383a.
Report an issue: GitHub.