Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Obsługa błędów w Server Actions

W Metropolis Quantum nawet najlepsze systemy mogą napotkać błędy — nieoczekiwane dane od użytkownika, problemy z bazą danych, czy awarie zewnętrznych API. Prawidłowa obsługa błędów w Server Actions to kluczowy element tworzenia niezawodnych aplikacji Next.js. W tej lekcji poznasz wzorce, które pozwolą Ci elegancko obsługiwać błędy i informować o nich użytkowników.

Try/catch w Server Actions

Podstawowym wzorcem obsługi błędów w Server Actions jest try/catch. Każda akcja serwerowa powinna przewidywać możliwość wystąpienia błędu:

1// app/actions.ts
2'use server';
3
4export async function createImplant(formData: FormData) {
5  try {
6    const name = formData.get('name') as string;
7    const powerLevel = Number(formData.get('powerLevel'));
8
9    // Operacja, która może się nie powieść
10    const result = await db.implants.create({ name, powerLevel });
11
12    return { success: true, data: result };
13  } catch (error) {
14    // NIGDY nie zwracaj surowego obiektu error do klienta!
15    console.error('Błąd tworzenia implantu:', error);
16    return {
17      success: false,
18      error: 'Nie udało się utworzyć implantu. Spróbuj ponownie.'
19    };
20  }
21}

Kluczowa zasada: Server Actions nie powinny rzucać wyjątków do klienta. Zamiast tego zwracaj ustrukturyzowany obiekt z informacją o sukcesie lub błędzie.

Wzorzec zwracania obiektów błędów

W profesjonalnych aplikacjach warto zdefiniować spójny typ dla odpowiedzi z Server Actions:

1// lib/types.ts
2type ActionState = {
3  status: 'idle' | 'success' | 'error';
4  message: string;
5  errors?: Record<string, string[]>; // Błędy per pole formularza
6};
7
8// app/actions.ts
9'use server';
10
11export async function updateProfile(
12  prevState: ActionState,
13  formData: FormData
14): Promise<ActionState> {
15  const name = formData.get('name') as string;
16  const email = formData.get('email') as string;
17
18  // Walidacja
19  const errors: Record<string, string[]> = {};
20
21  if (!name || name.length < 2) {
22    errors.name = ['Imię musi mieć co najmniej 2 znaki'];
23  }
24  if (!email || !email.includes('@')) {
25    errors.email = ['Nieprawidłowy format adresu email'];
26  }
27
28  if (Object.keys(errors).length > 0) {
29    return {
30      status: 'error',
31      message: 'Formularz zawiera błędy',
32      errors,
33    };
34  }
35
36  try {
37    await db.users.update({ name, email });
38    return { status: 'success', message: 'Profil zaktualizowany!' };
39  } catch (error) {
40    return { status: 'error', message: 'Błąd serwera. Spróbuj ponownie.' };
41  }
42}

Ten wzorzec — obiekt z polami

status
,
message
i opcjonalnym
errors
— pozwala klientowi precyzyjnie zrozumieć, co poszło nie tak.

useActionState do wyświetlania błędów

Hook

useActionState
(dawniej
useFormState
) to oficjalny sposób na łączenie Server Actions z interfejsem użytkownika w React/Next.js:

1'use client';
2
3import { useActionState } from 'react';
4import { updateProfile } from './actions';
5
6const initialState = {
7  status: 'idle',
8  message: '',
9  errors: {},
10};
11
12export default function ProfileForm() {
13  const [state, formAction, isPending] = useActionState(
14    updateProfile,
15    initialState
16  );
17
18  return (
19    <form action={formAction}>
20      <div>
21        <label htmlFor="name">Imię</label>
22        <input id="name" name="name" />
23        {state.errors?.name && (
24          <p className="text-red-500">{state.errors.name[0]}</p>
25        )}
26      </div>
27
28      <div>
29        <label htmlFor="email">Email</label>
30        <input id="email" name="email" type="email" />
31        {state.errors?.email && (
32          <p className="text-red-500">{state.errors.email[0]}</p>
33        )}
34      </div>
35
36      <button type="submit" disabled={isPending}>
37        {isPending ? 'Zapisywanie...' : 'Zapisz profil'}
38      </button>
39
40      {state.status === 'success' && (
41        <p className="text-green-500">{state.message}</p>
42      )}
43      {state.status === 'error' && !state.errors && (
44        <p className="text-red-500">{state.message}</p>
45      )}
46    </form>
47  );
48}

useActionState
przyjmuje trzy argumenty: akcję serwerową, stan początkowy, i zwraca tablicę: aktualny stan, funkcję formAction (przekazujemy ją do
action
formularza) oraz flagę
isPending
.

Walidacja z Zod w Server Actions

Połączenie Zod z Server Actions to potężny wzorzec — schemat walidacji jest współdzielony między klientem i serwerem:

1// lib/schemas.ts
2import { z } from 'zod';
3
4export const implantSchema = z.object({
5  name: z.string()
6    .min(2, 'Nazwa implantu musi mieć co najmniej 2 znaki')
7    .max(50, 'Nazwa implantu może mieć maksymalnie 50 znaków'),
8  powerLevel: z.coerce.number()
9    .min(1, 'Poziom mocy musi być większy od 0')
10    .max(100, 'Poziom mocy nie może przekraczać 100'),
11  type: z.enum(['neural', 'cybernetic', 'bionic'], {
12    errorMap: () => ({ message: 'Wybierz typ implantu' }),
13  }),
14});
15
16// app/actions.ts
17'use server';
18
19import { implantSchema } from '@/lib/schemas';
20
21export async function createImplant(prevState: any, formData: FormData) {
22  // Walidacja z Zod
23  const parsed = implantSchema.safeParse({
24    name: formData.get('name'),
25    powerLevel: formData.get('powerLevel'),
26    type: formData.get('type'),
27  });
28
29  if (!parsed.success) {
30    // Konwertujemy błędy Zod na format field -> messages
31    const fieldErrors: Record<string, string[]> = {};
32    for (const issue of parsed.error.issues) {
33      const field = issue.path[0] as string;
34      if (!fieldErrors[field]) fieldErrors[field] = [];
35      fieldErrors[field].push(issue.message);
36    }
37    return { status: 'error', message: 'Walidacja nie powiodła się', errors: fieldErrors };
38  }
39
40  try {
41    // parsed.data jest typowane i zwalidowane!
42    await db.implants.create(parsed.data);
43    return { status: 'success', message: 'Implant utworzony!' };
44  } catch (error) {
45    return { status: 'error', message: 'Błąd zapisu do bazy danych' };
46  }
47}

Zwróć uwagę na

z.coerce.number()
— konwertuje string z FormData na number przed walidacją. To kluczowe, bo
formData.get()
zawsze zwraca string.

Custom error types

W złożonych aplikacjach warto definiować własne typy błędów, by rozróżniać ich źródło:

1// lib/errors.ts
2export class ValidationError extends Error {
3  constructor(
4    public fieldErrors: Record<string, string[]>,
5    message = 'Walidacja nie powiodła się'
6  ) {
7    super(message);
8    this.name = 'ValidationError';
9  }
10}
11
12export class DatabaseError extends Error {
13  constructor(message = 'Błąd bazy danych') {
14    super(message);
15    this.name = 'DatabaseError';
16  }
17}
18
19export class AuthorizationError extends Error {
20  constructor(message = 'Brak uprawnień') {
21    super(message);
22    this.name = 'AuthorizationError';
23  }
24}
25
26// app/actions.ts
27'use server';
28
29import { ValidationError, DatabaseError, AuthorizationError } from '@/lib/errors';
30
31export async function deleteImplant(prevState: any, formData: FormData) {
32  try {
33    const id = formData.get('id') as string;
34
35    if (!id) {
36      throw new ValidationError({ id: ['ID implantu jest wymagane'] });
37    }
38
39    const user = await getCurrentUser();
40    if (!user?.isAdmin) {
41      throw new AuthorizationError('Tylko administratorzy mogą usuwać implanty');
42    }
43
44    await db.implants.delete(id);
45    return { status: 'success', message: 'Implant usunięty' };
46
47  } catch (error) {
48    if (error instanceof ValidationError) {
49      return { status: 'error', message: error.message, errors: error.fieldErrors };
50    }
51    if (error instanceof AuthorizationError) {
52      return { status: 'error', message: error.message };
53    }
54    if (error instanceof DatabaseError) {
55      return { status: 'error', message: 'Błąd systemu. Spróbuj ponownie później.' };
56    }
57    // Nieznany błąd — nie ujawniaj szczegółów użytkownikowi
58    console.error('Nieoczekiwany błąd:', error);
59    return { status: 'error', message: 'Wystąpił nieoczekiwany błąd' };
60  }
61}

Przyjazne komunikaty dla użytkownika

Komunikaty błędów powinny być zrozumiałe i pomocne — użytkownik musi wiedzieć, co zrobić:

1// Złe komunikaty:
2"Error: ECONNREFUSED 127.0.0.1:5432"  // Techniczny, niezrozumiały
3"Validation failed"                     // Za ogólny
4"null"                                  // Bezsensowny
5
6// Dobre komunikaty:
7"Nie udało się zapisać danych. Sprawdź połączenie i spróbuj ponownie."
8"Nazwa implantu musi mieć co najmniej 2 znaki."
9"Sesja wygasła. Zaloguj się ponownie, aby kontynuować."

Fallback behaviors

Gdy Server Action nie powiedzie się, aplikacja powinna mieć plan awaryjny:

1'use client';
2
3import { useActionState } from 'react';
4import { saveData } from './actions';
5
6export default function FormWithFallback() {
7  const [state, formAction, isPending] = useActionState(saveData, {
8    status: 'idle',
9    message: '',
10  });
11
12  // Fallback: retry po błędzie
13  const handleRetry = () => {
14    // Resetuj stan i pozwól użytkownikowi spróbować ponownie
15    window.location.reload();
16  };
17
18  if (state.status === 'error' && state.message.includes('serwera')) {
19    return (
20      <div>
21        <p>Wystąpił problem z serwerem.</p>
22        <button onClick={handleRetry}>Spróbuj ponownie</button>
23        <p>Jeśli problem się powtarza, skontaktuj się z administratorem.</p>
24      </div>
25    );
26  }
27
28  return (
29    <form action={formAction}>
30      {/* ... pola formularza ... */}
31      <button type="submit" disabled={isPending}>
32        {isPending ? 'Zapisywanie...' : 'Zapisz'}
33      </button>
34    </form>
35  );
36}

Podsumowanie

Obsługa błędów w Server Actions to krytyczny element niezawodnych aplikacji Next.js:

  • try/catch w każdej Server Action — nigdy nie pozwalaj, by surowe błędy dotarły do klienta
  • Ustrukturyzowane obiekty błędów
    { status, message, errors }
    zamiast rzucania wyjątków
  • useActionState — oficjalny hook do łączenia akcji z UI i wyświetlania błędów per pole
  • Zod w Server Actions
    safeParse()
    +
    z.coerce
    do walidacji danych z FormData
  • Custom error types — rozróżnianie źródła błędów (walidacja, autoryzacja, baza danych)
  • Przyjazne komunikaty — zrozumiałe, konkretne, z instrukcją co robić dalej
Przejdź do CodeWorlds