Skip to content
GitHubTwitterDiscord

Error Handling

LaunchKit provides comprehensive error handling to ensure a smooth user experience even when things go wrong.

Create a global error boundary that catches JavaScript errors:

// app/error.tsx
'use client';

import { useEffect } from 'react';
import ButtonSupport from '@/components/ButtonSupport';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Log error to monitoring service
    console.error('Application error:', error);

    // Optional: Send to error tracking service
    if (process.env.NODE_ENV === 'production') {
      // Example: Sentry, LogRocket, etc.
      // captureException(error);
    }
  }, [error]);

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md mx-auto text-center p-8">
        <div className="mb-6">
          <div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
            <svg
              className="w-8 h-8 text-red-600"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
              />
            </svg>
          </div>
          <h2 className="text-2xl font-bold text-gray-900 mb-2">
            Something went wrong!
          </h2>
          <p className="text-gray-600 mb-6">
            We apologize for the inconvenience. Our team has been notified and
            is working on a fix.
          </p>
        </div>

        <div className="space-y-3">
          <button
            onClick={reset}
            className="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
          >
            Try again
          </button>

          <ButtonSupport className="w-full bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
            Contact Support
          </ButtonSupport>

          <a
            href="/"
            className="block w-full text-blue-600 hover:text-blue-800 transition-colors"
          >
            Return to homepage
          </a>
        </div>
      </div>
    </div>
  );
}
// app/not-found.tsx
import Link from 'next/link';
import ButtonSupport from '@/components/ButtonSupport';

export default function NotFound() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md mx-auto text-center p-8">
        <div className="mb-6">
          <div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4">
            <span className="text-2xl font-bold text-blue-600">404</span>
          </div>
          <h2 className="text-2xl font-bold text-gray-900 mb-2">
            Page Not Found
          </h2>
          <p className="text-gray-600 mb-6">
            The page you're looking for doesn't exist or has been moved.
          </p>
        </div>

        <div className="space-y-3">
          <Link
            href="/"
            className="block w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
          >
            Go Home
          </Link>

          <ButtonSupport className="w-full bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
            Need Help?
          </ButtonSupport>
        </div>
      </div>
    </div>
  );
}

Create loading components for better UX:

// app/loading.tsx
export default function Loading() {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="text-center">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
        <p className="text-gray-600">Loading...</p>
      </div>
    </div>
  );
}

Standardize API error responses:

// types/api.ts
export interface ApiError {
  message: string;
  code?: string;
  field?: string;
  details?: any;
}

export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  error?: ApiError;
}

Create a wrapper for consistent error handling:

// libs/api-handler.ts
import { NextRequest, NextResponse } from 'next/server';
import { ApiResponse } from '@/types/api';

export function withErrorHandler(
  handler: (req: NextRequest) => Promise<NextResponse>
) {
  return async (req: NextRequest) => {
    try {
      return await handler(req);
    } catch (error) {
      console.error('API Error:', error);

      const response: ApiResponse = {
        success: false,
        error: {
          message:
            error instanceof Error ? error.message : 'Internal server error',
          code: 'INTERNAL_ERROR',
        },
      };

      return NextResponse.json(response, { status: 500 });
    }
  };
}
// app/api/leads/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { withErrorHandler } from '@/libs/api-handler';
import { ApiResponse } from '@/types/api';

async function handler(request: NextRequest) {
  if (request.method !== 'POST') {
    const response: ApiResponse = {
      success: false,
      error: {
        message: 'Method not allowed',
        code: 'METHOD_NOT_ALLOWED',
      },
    };
    return NextResponse.json(response, { status: 405 });
  }

  const { email } = await request.json();

  // Validation
  if (!email || typeof email !== 'string') {
    const response: ApiResponse = {
      success: false,
      error: {
        message: 'Email is required',
        code: 'VALIDATION_ERROR',
        field: 'email',
      },
    };
    return NextResponse.json(response, { status: 400 });
  }

  // Email format validation
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    const response: ApiResponse = {
      success: false,
      error: {
        message: 'Invalid email format',
        code: 'INVALID_EMAIL',
        field: 'email',
      },
    };
    return NextResponse.json(response, { status: 400 });
  }

  const cookieStore = cookies();
  const supabase = createRouteHandlerClient({ cookies: () => cookieStore });

  const { data, error } = await supabase
    .from('leads')
    .insert({ email })
    .select();

  if (error) {
    // Handle specific Supabase errors
    if (error.code === '23505') {
      // Unique constraint violation
      const response: ApiResponse = {
        success: false,
        error: {
          message: 'Email already exists',
          code: 'EMAIL_EXISTS',
          field: 'email',
        },
      };
      return NextResponse.json(response, { status: 409 });
    }

    throw new Error(`Database error: ${error.message}`);
  }

  const response: ApiResponse = {
    success: true,
    data: { lead: data[0] },
  };

  return NextResponse.json(response);
}

export const POST = withErrorHandler(handler);
// libs/api-client.ts
import { ApiResponse } from '@/types/api';

class ApiClient {
  private baseUrl: string;

  constructor(baseUrl = '/api') {
    this.baseUrl = baseUrl;
  }

  async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;

    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    });

    const data: ApiResponse<T> = await response.json();

    if (!data.success) {
      throw new ApiError(data.error?.message || 'Request failed', data.error);
    }

    return data.data as T;
  }

  async post<T>(endpoint: string, body: any): Promise<T> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(body),
    });
  }

  async get<T>(endpoint: string): Promise<T> {
    return this.request<T>(endpoint);
  }
}

export class ApiError extends Error {
  constructor(message: string, public details?: any) {
    super(message);
    this.name = 'ApiError';
  }
}

export const apiClient = new ApiClient();
// hooks/useErrorHandler.ts
import { useState, useCallback } from 'react';
import { ApiError } from '@/libs/api-client';

export function useErrorHandler() {
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const handleAsync = useCallback(
    async <T>(
      asyncFn: () => Promise<T>,
      onSuccess?: (result: T) => void,
      onError?: (error: Error) => void
    ) => {
      try {
        setIsLoading(true);
        setError(null);

        const result = await asyncFn();

        if (onSuccess) {
          onSuccess(result);
        }

        return result;
      } catch (err) {
        const errorMessage =
          err instanceof ApiError
            ? err.message
            : 'An unexpected error occurred';

        setError(errorMessage);

        if (onError) {
          onError(err as Error);
        }

        throw err;
      } finally {
        setIsLoading(false);
      }
    },
    []
  );

  const clearError = useCallback(() => {
    setError(null);
  }, []);

  return {
    error,
    isLoading,
    handleAsync,
    clearError,
  };
}
// components/ErrorMessage.tsx
interface ErrorMessageProps {
  message?: string | null;
  onDismiss?: () => void;
  className?: string;
}

export default function ErrorMessage({
  message,
  onDismiss,
  className = '',
}: ErrorMessageProps) {
  if (!message) return null;

  return (
    <div
      className={`bg-red-50 border border-red-200 rounded-md p-4 ${className}`}
    >
      <div className="flex">
        <div className="flex-shrink-0">
          <svg
            className="h-5 w-5 text-red-400"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fillRule="evenodd"
              d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
              clipRule="evenodd"
            />
          </svg>
        </div>
        <div className="ml-3">
          <p className="text-sm text-red-800">{message}</p>
        </div>
        {onDismiss && (
          <div className="ml-auto pl-3">
            <button
              onClick={onDismiss}
              className="text-red-400 hover:text-red-600"
            >
              <span className="sr-only">Dismiss</span>
              <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                <path
                  fillRule="evenodd"
                  d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
                  clipRule="evenodd"
                />
              </svg>
            </button>
          </div>
        )}
      </div>
    </div>
  );
}
// libs/error-tracking.ts
export function initErrorTracking() {
  if (process.env.NODE_ENV === 'production') {
    // Example: Sentry integration
    // import * as Sentry from '@sentry/nextjs';
    // Sentry.init({
    //   dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
    // });
  }
}

export function captureException(error: Error, context?: any) {
  if (process.env.NODE_ENV === 'production') {
    // Send to error tracking service
    console.error('Captured error:', error, context);
    // Sentry.captureException(error, { extra: context });
  } else {
    console.error('Development error:', error, context);
  }
}

This comprehensive error handling system ensures that your LaunchKit application gracefully handles errors and provides a great user experience even when things go wrong.