DEV Community

kanta13jp1
kanta13jp1

Posted on

Complete Error Handling Patterns for Supabase Edge Functions — 4-Stage Fallback Design

Complete Error Handling Patterns for Supabase Edge Functions

Why Error Handling Matters

Edge Functions have three error sources: external APIs, database access, and user input. Without proper handling:

  • Flutter clients crash on unexpected 500s
  • Supabase logs fill up with noise
  • Debugging takes forever

The 4-Stage Fallback Template

Deno.serve(async (req: Request) => {
  // Stage 1: CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders, status: 204 });
  }

  try {
    // Stage 2: Input validation
    const { action, ...params } = await parseBody(req);
    if (!action) {
      return errorResponse(400, 'action is required');
    }

    // Stage 3: Routing
    switch (action) {
      case 'get': return await handleGet(params);
      case 'upsert': return await handleUpsert(params);
      default: return errorResponse(400, `Unknown action: ${action}`);
    }
  } catch (err) {
    // Stage 4: Unexpected errors
    console.error('[EF Error]', err);
    return errorResponse(500, 'Internal server error');
  }
});
Enter fullscreen mode Exit fullscreen mode

Input Validation

async function parseBody(req: Request): Promise<Record<string, unknown>> {
  const contentType = req.headers.get('content-type') ?? '';

  if (contentType.includes('application/json')) {
    try {
      return await req.json();
    } catch {
      throw new ValidationError('Invalid JSON body');
    }
  }

  if (contentType.includes('application/x-www-form-urlencoded')) {
    const text = await req.text();
    return Object.fromEntries(new URLSearchParams(text));
  }

  return {};
}

class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}
Enter fullscreen mode Exit fullscreen mode

Unified Error Response Format

function errorResponse(status: number, message: string, details?: unknown): Response {
  return new Response(
    JSON.stringify({
      error: message,
      details: details ?? null,
      timestamp: new Date().toISOString(),
    }),
    {
      status,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    }
  );
}

function successResponse(data: unknown, status = 200): Response {
  return new Response(
    JSON.stringify({ data, success: true }),
    {
      status,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

Supabase Client Errors

async function handleGet(params: Record<string, unknown>): Promise<Response> {
  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
  );

  const { data, error } = await supabase
    .from('my_table')
    .select('*')
    .eq('id', params.id);

  if (error) {
    // Branch on PostgreSQL error code
    if (error.code === '42P01') { // table doesn't exist
      return errorResponse(500, 'Database schema error', error);
    }
    if (error.code === '23505') { // unique violation
      return errorResponse(409, 'Duplicate entry', error.details);
    }
    return errorResponse(500, error.message, error);
  }

  return successResponse(data);
}
Enter fullscreen mode Exit fullscreen mode

External API Calls with Timeout

async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 10000): Promise<Response> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal,
    });
    clearTimeout(timeoutId);
    return response;
  } catch (err) {
    clearTimeout(timeoutId);
    if (err instanceof Error && err.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

Keeping deno lint at Zero Errors

Common violations:

// ❌ no-explicit-any
async function handle(data: any) {}

// ✅ unknown + type guard
async function handle(data: unknown) {
  if (typeof data !== 'object' || data === null) throw new Error('Invalid data');
  const typed = data as Record<string, unknown>;
}

// ❌ prefer-const
let result = await fetch(url);

// ✅
const result = await fetch(url);
Enter fullscreen mode Exit fullscreen mode

Summary

Layer Defense
Input JSON/form support + type validation
Routing action switch + unknown action → 400
DB Branch on error.code
External API Timeout + AbortController
Unexpected try/catch → 500

Applying this 5-layer pattern across all hub EFs dramatically cuts debugging time in production.


Building in public: https://my-web-app-b67f4.web.app/

Supabase #Deno #TypeScript #buildinpublic #webdev

Top comments (0)