Fix: Next.js Server Action Not Working — Action Not Called or Returns Error
Quick Answer
How to fix Next.js Server Actions — use server directive, form binding, revalidation, error handling, middleware conflicts, and client component limitations.
The Problem
A Next.js Server Action doesn’t execute:
// app/actions.ts
'use server';
export async function createUser(formData: FormData) {
const name = formData.get('name');
await db.user.create({ data: { name } });
}
// app/page.tsx
<form action={createUser}>
<input name="name" />
<button type="submit">Create</button>
</form>
// Submitting the form does nothing — no network request, no errorOr the action runs but data doesn’t refresh:
// Action runs successfully (console.log shows the data)
// But the UI doesn't update — old data still showingOr a Server Action throws in a Client Component:
// Error: Server actions must be async functions
// Error: Failed to find Server Action "xxxx"
// Error: Functions cannot be passed directly to Client ComponentsWhy This Happens
Server Actions are a Next.js 14+ App Router feature. They require specific setup and have constraints that cause silent failures:
- Missing
'use server'directive — the directive must be at the top of the file (for a module of server actions) or at the top of the function body. Without it, the function is treated as a regular client-side function. - Action not revalidating cache — Next.js aggressively caches. After a Server Action mutates data, you must call
revalidatePath()orrevalidateTag()to refresh the cache and trigger a UI update. - Passing Server Actions to Client Components — a Server Action defined in a Server Component can be passed as a prop to a Client Component, but only if defined in a separate
'use server'file, not inline. useFormState/useActionStatenot set up correctly — for progressive enhancement with error handling, these hooks have specific binding requirements.- Middleware interfering — authentication middleware that returns early before the Server Action endpoint is reached blocks the action.
- Actions only work in App Router — Server Actions are not available in the Pages Router (
pages/).
Fix 1: Add the Correct ‘use server’ Directive
The 'use server' directive must appear at the top of the file or function:
// Option 1 — Module-level directive (entire file exports Server Actions)
// app/actions.ts
'use server'; // ← Must be the very first line
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function createUser(formData: FormData) {
const name = formData.get('name') as string;
await db.user.create({ data: { name } });
revalidatePath('/users');
}
export async function deleteUser(id: string) {
await db.user.delete({ where: { id } });
revalidatePath('/users');
}// Option 2 — Inline directive in Server Components
// app/page.tsx (Server Component — no 'use client')
export default function Page() {
async function createUser(formData: FormData) {
'use server'; // ← Inside the function body
const name = formData.get('name') as string;
await db.user.create({ data: { name } });
revalidatePath('/users');
}
return (
<form action={createUser}>
<input name="name" />
<button type="submit">Create</button>
</form>
);
}WRONG — no directive:
// WRONG — looks like a server action but isn't
export async function createUser(formData: FormData) {
// No 'use server' directive — runs on client, can't access db
await db.user.create({ data: { name } }); // db not available on client
}Fix 2: Revalidate After Mutations
After data changes, tell Next.js to invalidate the cache:
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const post = await db.post.create({ data: { title } });
// Revalidate specific paths
revalidatePath('/posts'); // Revalidate the posts list page
revalidatePath(`/posts/${post.id}`); // Revalidate the new post page
// Or revalidate by cache tag (if you tagged fetches)
revalidateTag('posts');
// Redirect after successful creation
redirect(`/posts/${post.id}`);
}
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
await db.post.update({ where: { id }, data: { title } });
revalidatePath('/posts');
revalidatePath(`/posts/${id}`);
}Tag-based revalidation for fine-grained cache control:
// app/posts/page.tsx — fetch with cache tag
const posts = await fetch('/api/posts', {
next: { tags: ['posts'] }
}).then(r => r.json());
// In Server Action — invalidate by tag
revalidateTag('posts'); // All fetches tagged 'posts' are revalidatedFix 3: Use Server Actions in Client Components
Server Actions defined in 'use server' files can be imported into Client Components:
// app/actions.ts — separate server actions file
'use server';
export async function createUser(formData: FormData) {
await db.user.create({ data: {
name: formData.get('name') as string,
}});
revalidatePath('/users');
}// app/components/UserForm.tsx — Client Component
'use client';
import { createUser } from '@/app/actions'; // Import server action
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus(); // Shows loading state during action
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create User'}
</button>
);
}
export function UserForm() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<SubmitButton />
</form>
);
}WRONG — defining inline Server Action inside a Client Component:
// WRONG — can't define 'use server' inside a Client Component
'use client';
export function UserForm() {
async function createUser(formData: FormData) {
'use server'; // Error: Server Actions cannot be defined inside Client Components
await db.user.create(...);
}
return <form action={createUser}>...</form>;
}Fix 4: Error Handling with useActionState
Handle Server Action errors and show feedback to users:
// app/actions.ts
'use server';
type ActionState = {
error?: string;
success?: boolean;
};
export async function createUser(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const name = formData.get('name') as string;
if (!name || name.trim().length < 2) {
return { error: 'Name must be at least 2 characters' };
}
try {
await db.user.create({ data: { name } });
revalidatePath('/users');
return { success: true };
} catch (err) {
return { error: 'Failed to create user. Please try again.' };
}
}// app/components/UserForm.tsx
'use client';
import { useActionState } from 'react'; // React 19 / Next.js 14+
import { createUser } from '@/app/actions';
export function UserForm() {
const [state, formAction, isPending] = useActionState(createUser, {});
return (
<form action={formAction}>
{state.error && (
<p className="text-red-500">{state.error}</p>
)}
{state.success && (
<p className="text-green-500">User created successfully!</p>
)}
<input name="name" placeholder="Name" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}Note:
useActionStatewas added in React 19. In Next.js 14 with React 18, useuseFormStatefromreact-dominstead (same API, different import).
Fix 5: Programmatic Invocation (Without Forms)
Server Actions can be called programmatically, not just via form submission:
// app/components/DeleteButton.tsx
'use client';
import { deleteUser } from '@/app/actions';
import { useTransition } from 'react';
export function DeleteButton({ userId }: { userId: string }) {
const [isPending, startTransition] = useTransition();
function handleDelete() {
startTransition(async () => {
await deleteUser(userId);
});
}
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}With optimistic updates:
'use client';
import { useOptimistic } from 'react';
import { deleteUser } from '@/app/actions';
export function UserList({ users }: { users: User[] }) {
const [optimisticUsers, removeOptimisticUser] = useOptimistic(
users,
(state, userId: string) => state.filter(u => u.id !== userId)
);
async function handleDelete(userId: string) {
removeOptimisticUser(userId); // Instant UI update
await deleteUser(userId); // Actual server mutation
}
return (
<ul>
{optimisticUsers.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => handleDelete(user.id)}>Delete</button>
</li>
))}
</ul>
);
}Fix 6: Authentication in Server Actions
Protect Server Actions from unauthorized use:
// app/actions.ts
'use server';
import { auth } from '@/lib/auth'; // Your auth solution (NextAuth, Clerk, etc.)
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
// Verify authentication inside the action — don't rely on middleware alone
const session = await auth();
if (!session?.user) {
redirect('/login');
// Or: throw new Error('Unauthorized');
}
const title = formData.get('title') as string;
await db.post.create({
data: {
title,
authorId: session.user.id, // Use verified session data
},
});
revalidatePath('/posts');
}Warning: Never trust data from
formDatafor sensitive operations like setting theauthorId. Always use the server-side session to determine who’s performing the action. A malicious client could send any value in a hidden form field.
Still Not Working?
Server Actions require Next.js 14+ App Router — they don’t work in the pages/ directory or with Next.js 13 without experimental flags. Verify your next.config.js:
// next.config.js — Server Actions are stable in Next.js 14+
// No configuration needed. In Next.js 13, they required:
// experimental: { serverActions: true }redirect() inside try-catch — redirect() throws internally. If you catch all errors, you’ll catch the redirect too:
// WRONG — redirect gets caught
try {
await db.post.create(...);
redirect('/posts'); // Throws internally
} catch (err) {
// This catches the redirect!
return { error: 'Something went wrong' };
}
// CORRECT — redirect outside try-catch
let redirectPath: string | null = null;
try {
await db.post.create(...);
redirectPath = '/posts';
} catch (err) {
return { error: 'Failed to create post' };
}
if (redirectPath) redirect(redirectPath);Action endpoint 404 in development — if you rename or move a Server Action file, the client may have a cached reference to the old endpoint URL. Hard-refresh the browser (Ctrl+Shift+R) to clear the client-side module cache.
For related Next.js issues, see Fix: Next.js API Route Not Working and Fix: Next.js Middleware Not Running.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: TanStack Start Not Working — Server Functions Failing, Routes Not Loading, or SSR Errors
How to fix TanStack Start issues — project setup, file-based routing, server functions with createServerFn, data loading, middleware, SSR hydration, and deployment configuration.
Fix: Waku Not Working — Server Components Not Rendering, Client Components Not Interactive, or Build Errors
How to fix Waku React framework issues — RSC setup, server and client component patterns, data fetching, routing, layout system, and deployment configuration.
Fix: Wasp Not Working — Build Failing, Auth Not Working, or Operations Returning Empty
How to fix Wasp full-stack framework issues — main.wasp configuration, queries and actions, authentication setup, Prisma integration, job scheduling, and deployment.
Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.