Back to Blog
Next.jsJuly 8, 20264 min read

Next.js Server Actions Error Handling & Global Toasts Guide

Master Next.js Server Actions error handling with useActionState and global toasts. Build a robust, scalable pattern for cleaner form mutations.

Next.jsReactWeb DevelopmentServer ActionsError Handling

Getting error handling right in the Next.js App Router is often the difference between a polished product and one that feels "broken" under the hood. When I first started migrating forms to Server Actions, I relied on basic try/catch blocks inside components, which quickly turned into a maintenance nightmare.

If you’re building a Next.js Full-Stack Web App Development project, you need a predictable way to bridge server-side failures with client-side UI feedback. Here’s how I structure my error handling and global notifications to keep the codebase clean.

The Strategy: Standardizing Server Responses

The biggest mistake I made early on was returning different shapes of data from different Server Actions. Some returned null, some returned a boolean, and others threw errors that crashed the UI boundary.

To fix this, I enforce a strict return type for all my actions. Every action returns an object that follows a predictable schema.

TYPESCRIPT
// types/actions.ts
export type ActionResponse<T = any> = {
  success: boolean;
  message: string;
  data?: T;
  errors?: Record<string, string[]>;
};

By standardizing this, I can write a single hook or utility to parse these results, regardless of which form triggered the action. If you’re dealing with complex workflows, remember that Next.js Server Actions: Implementing Saga Pattern Orchestration can help manage these states across distributed transactions.

Implementing React useActionState

In Next.js 15, useFormState was renamed to useActionState, but the core functionality remains the same. This hook is the glue between your Server Action and your UI.

Here is a typical implementation for a profile update form:

TSX
CE9178">'use client';
import { useActionState } from CE9178">'react';
import { updateProfile } from CE9178">'./actions';
import { toast } from CE9178">'sonner';

export function ProfileForm() {
  const [state, formAction, isPending] = useActionState(updateProfile, {
    success: false,
    message: CE9178">'',
  });

  // Handle global notifications based on state change
  useEffect(() => {
    if (state.message) {
      if (state.success) {
        toast.success(state.message);
      } else {
        toast.error(state.message);
      }
    }
  }, [state]);

  return (
    <form action={formAction}>
      {/* Inputs go here */}
      <button disabled={isPending}>Save</button>
    </form>
  );
}

Why Global Toast Notifications Matter

When implementing Next.js Server Actions and server-side validation, you shouldn't manually trigger toasts inside every single action. That couples your business logic to your UI library.

Instead, let the Server Action return the status, and let the component (or a specialized hook) handle the side effect. This keeps your actions testable and focused on data integrity. For example, when you combine this with Next.js App Router: Mastering useOptimistic for Atomic Mutations, the user gets immediate feedback while the server works in the background.

Comparison of Error Handling Approaches

ApproachProsCons
try/catch in UIQuick to writeLogic duplication
useActionStatePredictable, standardRequires boilerplate
Error BoundariesHandles crashesNot for form validation

Handling Server-Side Validation Errors

I prefer using Zod for validation. When validation fails, I return the specific field errors back to the client. This allows me to highlight the exact input that failed, rather than just throwing a generic "invalid input" error.

TYPESCRIPT
// actions.ts
export async function updateProfile(prevState: any, formData: FormData) {
  const result = profileSchema.safeParse(Object.fromEntries(formData));

  if (!result.success) {
    return {
      success: false,
      message: "Validation failed",
      errors: result.error.flatten().fieldErrors,
    };
  }

  // Proceed with database update...
  return { success: true, message: "Profile updated!" };
}

FAQ: Common Pitfalls

Can I use useActionState with multiple forms? Yes, but you'll need to define a separate state for each form instance. Don't try to share the same state object across the entire page unless those forms are logically linked.

Do I still need Error Boundaries? Yes. useActionState handles expected errors (validation, auth, DB constraints). Error Boundaries are still your safety net for unexpected runtime exceptions that shouldn't crash the entire app.

How do I clear the state after a success? The useActionState hook is designed to persist the last result. If you need to clear it, consider using a key prop on your form to force a re-render or reset the state on a successful navigation event.

Moving to this pattern took me about three days of refactoring across a mid-sized dashboard, but it drastically reduced the "where did this error message come from?" debugging sessions. I'm still experimenting with whether it's better to pass the toast function into a custom hook or keep it inside the component, but for now, the local useEffect approach feels the most readable.

Similar Posts