How to debug Next.js Server Actions
A Server Action looks like a function call, but it is an HTTP request in disguise. Once you know what that request looks like, Server Actions are much easier to debug.
What a Server Action sends
Calling a Server Action, from a form or from code, makes the browser send a POST to the current page's URL with a Next-Action header that identifies the action. The body holds the serialized arguments (form data for forms), and the response is an RSC payload (text/x-component). In the browser's Network tab, look for a POST to the page itself.
Where its logs and errors go
The action runs on the server, so console.log inside it prints in the terminal, not the browser. In development an error thrown inside it shows in the error overlay. In production the message is hidden from the client, so return errors as data instead of throwing for anything the user should see:
'use server';
export async function createTask(formData: FormData) {
const title = String(formData.get('title') ?? '');
if (!title) return { error: 'Title is required' };
await db.task.create({ data: { title } });
revalidatePath('/tasks');
return { ok: true };
}
Breakpoints
Server Actions are ordinary server code, so a breakpoint in the action pauses with the Node.js inspector (NODE_OPTIONS='--inspect' next dev) or with qaisdebugs, where the action's request, its logs and every call it makes appear together.
Common pitfalls
The page does not update
After changing data, call revalidatePath or revalidateTag, or the page keeps showing the old result.
redirect() inside try/catch
redirect() works by throwing a special error. If your catch catches it, the redirect never happens. Call redirect() after the try/catch.
Request body too large
Server Actions accept 1 MB by default. For larger uploads, raise serverActions.bodySizeLimit in next.config or upload in another way.
Trusting the input
An action is a public endpoint. Validate its arguments and check the user's permission inside it, just like an API route.