---
name: error-handling
description: Implement consistent error handling patterns across Mawidi including API errors, React error boundaries, form validation, and user-friendly error messages. Use when adding error handling to new features, improving error UX, or debugging error flows.
---

# Error Handling Skill

## Overview

Implement consistent, user-friendly error handling across Mawidi. This skill covers API error responses, React error boundaries, form validation errors, and bilingual error messages.

**Goals:**

- Consistent error format across all APIs
- User-friendly error messages (Arabic + English)
- Graceful degradation for failures
- Comprehensive error logging

## When to Use

Use this skill when:

- Adding error handling to new features
- Creating API endpoints
- Building forms with validation
- Implementing error boundaries
- Improving error UX
- Debugging error flows

## API Error Responses

### Standard Error Format

```typescript
// lib/types/api-error.ts

export interface APIError {
  error: string; // User-friendly message
  code?: string; // Machine-readable code
  details?: unknown; // Additional context
  statusCode: number; // HTTP status
}

// Example responses
const errors = {
  unauthorized: {
    error: "You must be logged in to perform this action",
    code: "UNAUTHORIZED",
    statusCode: 401,
  },
  validation: {
    error: "Please fix the following errors",
    code: "VALIDATION_ERROR",
    details: {
      email: "Invalid email format",
      phone: "Phone number is required",
    },
    statusCode: 400,
  },
  notFound: {
    error: "The requested resource was not found",
    code: "NOT_FOUND",
    statusCode: 404,
  },
};
```

### Error Response Helper

```typescript
// lib/utils/api-response.util.ts

import { NextResponse } from "next/server";

export function errorResponse(
  error: string,
  statusCode: number,
  options?: { code?: string; details?: unknown },
) {
  return NextResponse.json(
    {
      error,
      code: options?.code,
      details: options?.details,
    },
    { status: statusCode },
  );
}

// Usage
return errorResponse("Invalid email format", 400, {
  code: "VALIDATION_ERROR",
  details: { email: "Must be a valid email address" },
});
```

### Try-Catch Pattern for API Routes

```typescript
// app/api/bookings/route.ts

import { NextRequest } from "next/server";
import { errorResponse, successResponse } from "@/lib/utils/api-response.util";
import { logger } from "@/lib/logger";

export async function POST(request: NextRequest) {
  try {
    // Validate request
    const body = await request.json().catch(() => null);
    if (!body) {
      return errorResponse("Invalid JSON body", 400, {
        code: "INVALID_JSON",
      });
    }

    // Validate input
    const validation = bookingSchema.safeParse(body);
    if (!validation.success) {
      return errorResponse("Validation failed", 400, {
        code: "VALIDATION_ERROR",
        details: validation.error.flatten().fieldErrors,
      });
    }

    // Business logic
    const booking = await createBooking(validation.data);

    return successResponse(booking, 201);
  } catch (error: any) {
    // Log detailed error
    logger.error("POST /api/bookings failed", {
      error: error.message,
      stack: error.stack,
      body: await request.text().catch(() => "Unable to read body"),
    });

    // Handle known errors
    if (error.code === "P2002") {
      // Prisma unique constraint
      return errorResponse("A booking already exists for this time slot", 409, {
        code: "DUPLICATE_BOOKING",
      });
    }

    if (error.code === "P2025") {
      // Prisma not found
      return errorResponse("Service or user not found", 404, {
        code: "NOT_FOUND",
      });
    }

    // Generic error
    return errorResponse("An unexpected error occurred", 500, {
      code: "INTERNAL_ERROR",
    });
  }
}
```

## Form Validation Errors

### Zod Validation with Error Messages

```typescript
// lib/validators/booking.validator.ts

import { z } from "zod";

export const bookingSchema = z.object({
  date: z.string().datetime({ message: "Please select a valid date" }),

  serviceId: z.string().uuid({ message: "Please select a service" }),

  notes: z
    .string()
    .max(500, { message: "Notes cannot exceed 500 characters" })
    .optional(),

  patientPhone: z.string().regex(/^\+?[1-9]\d{1,14}$/, {
    message: "Please enter a valid phone number",
  }),
});

// Bilingual error messages
export const bookingSchemaAr = z.object({
  date: z.string().datetime({ message: "يرجى اختيار تاريخ صحيح" }),

  serviceId: z.string().uuid({ message: "يرجى اختيار خدمة" }),

  patientPhone: z.string().regex(/^\+?[1-9]\d{1,14}$/, {
    message: "يرجى إدخال رقم هاتف صحيح",
  }),
});
```

### React Hook Form Error Display

```tsx
// components/forms/BookingForm.tsx

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { bookingSchema } from "@/lib/validators/booking.validator";

export function BookingForm({ lang }: { lang: "ar" | "en" }) {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(bookingSchema),
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label htmlFor="date">{lang === "ar" ? "التاريخ" : "Date"}</label>
        <input
          id="date"
          type="datetime-local"
          {...register("date")}
          aria-invalid={errors.date ? "true" : "false"}
          aria-describedby={errors.date ? "date-error" : undefined}
        />
        {errors.date && (
          <p id="date-error" className="text-red-500 text-sm mt-1" role="alert">
            {errors.date.message}
          </p>
        )}
      </div>

      <div>
        <label htmlFor="phone">
          {lang === "ar" ? "رقم الهاتف" : "Phone Number"}
        </label>
        <input
          id="phone"
          type="tel"
          {...register("patientPhone")}
          aria-invalid={errors.patientPhone ? "true" : "false"}
        />
        {errors.patientPhone && (
          <p className="text-red-500 text-sm mt-1" role="alert">
            {errors.patientPhone.message}
          </p>
        )}
      </div>

      <button type="submit">{lang === "ar" ? "احجز الآن" : "Book Now"}</button>
    </form>
  );
}
```

### Field Error Component

```tsx
// components/ui/FieldError.tsx

interface FieldErrorProps {
  error?: string;
  id?: string;
}

export function FieldError({ error, id }: FieldErrorProps) {
  if (!error) return null;

  return (
    <p
      id={id}
      className="text-red-500 text-sm mt-1 flex items-center gap-1"
      role="alert"
    >
      <AlertCircle className="w-4 h-4" />
      {error}
    </p>
  );
}
```

## React Error Boundaries

### Global Error Boundary

```tsx
// app/error.tsx

"use client";

import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { logger } from "@/lib/logger";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log error to monitoring service
    logger.error("Unhandled error", {
      message: error.message,
      stack: error.stack,
      digest: error.digest,
    });
  }, [error]);

  return (
    <div className="min-h-screen flex items-center justify-center p-4">
      <div className="text-center max-w-md">
        <h2 className="text-2xl font-bold text-gray-900 mb-4">
          Something went wrong
        </h2>
        <p className="text-gray-600 mb-6">
          We're sorry, but something unexpected happened. Please try again or
          contact support if the problem persists.
        </p>
        <div className="space-x-4">
          <Button onClick={reset}>Try Again</Button>
          <Button
            variant="outline"
            onClick={() => (window.location.href = "/")}
          >
            Go Home
          </Button>
        </div>
      </div>
    </div>
  );
}
```

### Bilingual Error Page

```tsx
// app/[lang]/error.tsx

"use client";

import { useParams } from "next/navigation";

const content = {
  en: {
    title: "Something went wrong",
    description: "We apologize for the inconvenience. Please try again.",
    tryAgain: "Try Again",
    goHome: "Go Home",
  },
  ar: {
    title: "حدث خطأ ما",
    description: "نعتذر عن الإزعاج. يرجى المحاولة مرة أخرى.",
    tryAgain: "حاول مرة أخرى",
    goHome: "العودة للرئيسية",
  },
};

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  const params = useParams();
  const lang = (params.lang as "ar" | "en") || "en";
  const t = content[lang];

  return (
    <div
      className="min-h-screen flex items-center justify-center p-4"
      dir={lang === "ar" ? "rtl" : "ltr"}
    >
      <div className="text-center max-w-md">
        <h2 className="text-2xl font-bold mb-4">{t.title}</h2>
        <p className="text-gray-600 mb-6">{t.description}</p>
        <div className="space-x-4 rtl:space-x-reverse">
          <Button onClick={reset}>{t.tryAgain}</Button>
          <Button
            variant="outline"
            onClick={() => (window.location.href = `/${lang}`)}
          >
            {t.goHome}
          </Button>
        </div>
      </div>
    </div>
  );
}
```

### Not Found Page

```tsx
// app/[lang]/not-found.tsx

import Link from "next/link";
import { Button } from "@/components/ui/button";

const content = {
  en: {
    title: "Page Not Found",
    description: "The page you're looking for doesn't exist or has been moved.",
    goHome: "Go to Homepage",
    goBack: "Go Back",
  },
  ar: {
    title: "الصفحة غير موجودة",
    description: "الصفحة التي تبحث عنها غير موجودة أو تم نقلها.",
    goHome: "الذهاب للرئيسية",
    goBack: "العودة",
  },
};

export default function NotFound({
  params,
}: {
  params: { lang: "ar" | "en" };
}) {
  const lang = params?.lang || "en";
  const t = content[lang];

  return (
    <div
      className="min-h-screen flex items-center justify-center p-4"
      dir={lang === "ar" ? "rtl" : "ltr"}
    >
      <div className="text-center">
        <h1 className="text-6xl font-bold text-mawidi-green mb-4">404</h1>
        <h2 className="text-2xl font-semibold mb-2">{t.title}</h2>
        <p className="text-gray-600 mb-8">{t.description}</p>
        <div className="space-x-4 rtl:space-x-reverse">
          <Button asChild>
            <Link href={`/${lang}`}>{t.goHome}</Link>
          </Button>
          <Button variant="outline" onClick={() => history.back()}>
            {t.goBack}
          </Button>
        </div>
      </div>
    </div>
  );
}
```

## Client-Side Error Handling

### API Request Error Handling

```typescript
// lib/api/client.ts

export class APIError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public code?: string,
    public details?: unknown,
  ) {
    super(message);
    this.name = "APIError";
  }
}

export async function apiRequest<T>(
  url: string,
  options?: RequestInit,
): Promise<T> {
  try {
    const response = await fetch(url, {
      ...options,
      headers: {
        "Content-Type": "application/json",
        ...options?.headers,
      },
    });

    const data = await response.json();

    if (!response.ok) {
      throw new APIError(
        data.error || "An error occurred",
        response.status,
        data.code,
        data.details,
      );
    }

    return data;
  } catch (error) {
    if (error instanceof APIError) {
      throw error;
    }

    // Network or parsing error
    throw new APIError("Unable to connect to the server", 0, "NETWORK_ERROR");
  }
}
```

### Error Handling in React Query

```typescript
// hooks/useBookings.ts

import { useQuery, useMutation } from "@tanstack/react-query";
import { apiRequest, APIError } from "@/lib/api/client";
import { useToast } from "@/hooks/use-toast";

export function useCreateBooking() {
  const { toast } = useToast();

  return useMutation({
    mutationFn: (data: CreateBookingInput) =>
      apiRequest("/api/bookings", {
        method: "POST",
        body: JSON.stringify(data),
      }),

    onError: (error: APIError) => {
      // Show user-friendly toast
      toast({
        title: "Booking Failed",
        description: error.message,
        variant: "destructive",
      });

      // Handle specific error codes
      if (error.code === "DUPLICATE_BOOKING") {
        // Redirect to existing booking
      }
    },

    onSuccess: () => {
      toast({
        title: "Booking Confirmed",
        description: "Your appointment has been scheduled.",
      });
    },
  });
}
```

## Error Logging

### Logger Setup

```typescript
// lib/logger.ts

type LogLevel = "debug" | "info" | "warn" | "error";

interface LogContext {
  [key: string]: unknown;
}

class Logger {
  private log(level: LogLevel, message: string, context?: LogContext) {
    const timestamp = new Date().toISOString();
    const logEntry = {
      timestamp,
      level,
      message,
      ...context,
    };

    // In development, log to console
    if (process.env.NODE_ENV === "development") {
      console[level](JSON.stringify(logEntry, null, 2));
      return;
    }

    // In production, send to logging service
    // Example: Send to Sentry, LogRocket, etc.
    this.sendToService(logEntry);
  }

  private sendToService(entry: any) {
    // Implement your logging service integration
    // fetch('/api/logs', { method: 'POST', body: JSON.stringify(entry) });
  }

  debug(message: string, context?: LogContext) {
    this.log("debug", message, context);
  }

  info(message: string, context?: LogContext) {
    this.log("info", message, context);
  }

  warn(message: string, context?: LogContext) {
    this.log("warn", message, context);
  }

  error(message: string, context?: LogContext) {
    this.log("error", message, context);
  }
}

export const logger = new Logger();
```

## Error Messages (Bilingual)

```typescript
// lib/errors/messages.ts

export const errorMessages = {
  UNAUTHORIZED: {
    en: "You must be logged in to perform this action",
    ar: "يجب تسجيل الدخول للقيام بهذا الإجراء",
  },
  FORBIDDEN: {
    en: "You do not have permission to perform this action",
    ar: "ليس لديك صلاحية للقيام بهذا الإجراء",
  },
  NOT_FOUND: {
    en: "The requested resource was not found",
    ar: "لم يتم العثور على المورد المطلوب",
  },
  VALIDATION_ERROR: {
    en: "Please fix the errors below",
    ar: "يرجى تصحيح الأخطاء أدناه",
  },
  DUPLICATE_BOOKING: {
    en: "A booking already exists for this time slot",
    ar: "يوجد حجز بالفعل لهذا الوقت",
  },
  NETWORK_ERROR: {
    en: "Unable to connect to the server. Please check your internet connection.",
    ar: "تعذر الاتصال بالخادم. يرجى التحقق من اتصالك بالإنترنت.",
  },
  INTERNAL_ERROR: {
    en: "An unexpected error occurred. Please try again later.",
    ar: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.",
  },
};

export function getErrorMessage(code: string, lang: "ar" | "en"): string {
  return errorMessages[code]?.[lang] || errorMessages.INTERNAL_ERROR[lang];
}
```

## Quick Reference

### HTTP Status Codes

| Code | Meaning           | When to Use                      |
| ---- | ----------------- | -------------------------------- |
| 400  | Bad Request       | Invalid input, validation failed |
| 401  | Unauthorized      | Not authenticated                |
| 403  | Forbidden         | Not authorized for action        |
| 404  | Not Found         | Resource doesn't exist           |
| 409  | Conflict          | Duplicate, already exists        |
| 422  | Unprocessable     | Semantic errors                  |
| 429  | Too Many Requests | Rate limited                     |
| 500  | Internal Error    | Unexpected server error          |

### Error Response Template

```typescript
return NextResponse.json(
  {
    error: "User-friendly message",
    code: "MACHINE_READABLE_CODE",
    details: { field: "Specific issue" },
  },
  { status: 400 },
);
```

## See Also

- [api-development](../api-development/SKILL.md) - API patterns
- [testing-patterns](../testing-patterns/SKILL.md) - Testing error handling
- [i18n-management](../i18n-management/SKILL.md) - Bilingual messages
