Skip to content

Fix: Next.js Server Action Not Working — Action Not Called or Returns Error

FixDevs ·

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 error

Or 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 showing

Or 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 Components

Why 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() or revalidateTag() 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 / useActionState not 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 revalidated

Fix 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: useActionState was added in React 19. In Next.js 14 with React 18, use useFormState from react-dom instead (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 formData for sensitive operations like setting the authorId. 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-catchredirect() 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles