$ cat /posts/supabase-error-handling-best-practices-and-debugging.md
[tags]Supabase

Supabase Error Handling: Best Practices and Debugging

drwxr-xr-x2026-01-275 min0 views
Supabase Error Handling: Best Practices and Debugging

Mastering error handling in Supabase applications ensures robust, user-friendly experiences through comprehensive error catching, meaningful error messages, proper logging, graceful degradation, and effective debugging strategies enabling applications to handle failures elegantly while providing developers with actionable insights for resolution. Unlike applications with poor error handling displaying cryptic messages, crashing unexpectedly, losing user data, providing no debugging information, and frustrating users, well-designed error handling implements try-catch blocks catching exceptions gracefully, type-specific error handling providing context-aware responses, user-friendly messages avoiding technical jargon, error logging capturing details for debugging, retry logic handling transient failures, and monitoring systems alerting teams immediately. This comprehensive guide covers understanding Supabase error types and structures, implementing try-catch patterns with async/await, handling authentication errors gracefully, managing database query errors, dealing with storage upload failures, implementing error boundaries in React, creating error logging systems, debugging with browser DevTools, and building resilient applications with retry logic. Error handling demonstrates production-ready development ensuring applications remain stable and maintainable. Before starting, review JavaScript client basics, Next.js integration, and monitoring practices.

Supabase Error Types

Error CategoryCommon CausesHTTP Status
AuthenticationInvalid credentials, expired token, missing session401, 403
DatabaseRLS policy violation, constraint violation, invalid query400, 403
StorageFile size limit, invalid bucket, RLS policy400, 413
NetworkTimeout, connection failed, DNS error408, 502, 503
ValidationInvalid data format, missing fields, type mismatch400
Rate LimitingToo many requests429
ServerInternal server error, database down500, 503

Basic Error Handling Patterns

typescriptbasic_error_handling.ts
// Basic try-catch pattern
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// BEFORE: No error handling (BAD)
async function getUsers() {
  const { data } = await supabase.from('users').select('*')
  return data  // What if error occurs?
}

// AFTER: Proper error handling (GOOD)
async function getUsersWithErrorHandling() {
  try {
    const { data, error } = await supabase.from('users').select('*')
    
    if (error) {
      console.error('Database error:', error)
      throw new Error(`Failed to fetch users: ${error.message}`)
    }
    
    return data
  } catch (error) {
    console.error('Unexpected error:', error)
    throw error  // Re-throw for upstream handling
  }
}

// Check both data and error
async function safeQuery() {
  const { data, error } = await supabase
    .from('posts')
    .select('*')
    .eq('published', true)
  
  // Always check error first
  if (error) {
    console.error('Query failed:', error.message)
    return { success: false, error: error.message, data: null }
  }
  
  // Then check data
  if (!data || data.length === 0) {
    return { success: true, error: null, data: [] }
  }
  
  return { success: true, error: null, data }
}

// Type-safe error handling
interface DatabaseError {
  message: string
  details?: string
  hint?: string
  code?: string
}

function handleDatabaseError(error: DatabaseError): string {
  // Log full error for debugging
  console.error('Database error:', {
    message: error.message,
    details: error.details,
    hint: error.hint,
    code: error.code,
  })
  
  // Return user-friendly message
  switch (error.code) {
    case '23505':  // Unique violation
      return 'This record already exists'
    case '23503':  // Foreign key violation
      return 'Referenced record does not exist'
    case '42P01':  // Undefined table
      return 'Database configuration error'
    case 'PGRST116':  // RLS policy violation
      return 'You do not have permission to perform this action'
    default:
      return 'An error occurred. Please try again.'
  }
}

// Usage
async function createUser(userData: any) {
  const { data, error } = await supabase
    .from('users')
    .insert(userData)
    .select()
    .single()
  
  if (error) {
    const userMessage = handleDatabaseError(error)
    throw new Error(userMessage)
  }
  
  return data
}

// Error wrapper for consistent handling
async function withErrorHandling<T>(
  operation: () => Promise<{ data: T | null; error: any }>,
  context: string
): Promise<T> {
  try {
    const { data, error } = await operation()
    
    if (error) {
      console.error(`${context} failed:`, error)
      throw new Error(`${context}: ${error.message}`)
    }
    
    if (!data) {
      throw new Error(`${context}: No data returned`)
    }
    
    return data
  } catch (error) {
    console.error(`${context} error:`, error)
    throw error
  }
}

// Usage
const users = await withErrorHandling(
  () => supabase.from('users').select('*'),
  'Fetching users'
)

Authentication Error Handling

typescriptauth_error_handling.ts
// Authentication error types
interface AuthError {
  message: string
  status?: number
  name?: string
}

// Sign up error handling
async function signUp(email: string, password: string) {
  try {
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
    })
    
    if (error) {
      // Handle specific auth errors
      if (error.message.includes('already registered')) {
        throw new Error('This email is already registered. Please sign in instead.')
      }
      
      if (error.message.includes('Password should be')) {
        throw new Error('Password must be at least 8 characters long.')
      }
      
      if (error.message.includes('email')) {
        throw new Error('Please enter a valid email address.')
      }
      
      // Generic error
      throw new Error('Sign up failed. Please try again.')
    }
    
    if (!data.user) {
      throw new Error('Sign up succeeded but no user data returned.')
    }
    
    return { user: data.user, session: data.session }
  } catch (error) {
    console.error('Sign up error:', error)
    throw error
  }
}

// Sign in error handling
async function signIn(email: string, password: string) {
  try {
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })
    
    if (error) {
      if (error.message.includes('Invalid login credentials')) {
        throw new Error('Invalid email or password.')
      }
      
      if (error.message.includes('Email not confirmed')) {
        throw new Error('Please verify your email before signing in.')
      }
      
      throw new Error('Sign in failed. Please try again.')
    }
    
    return { user: data.user, session: data.session }
  } catch (error) {
    console.error('Sign in error:', error)
    throw error
  }
}

// Session handling with error recovery
async function getSession() {
  try {
    const { data, error } = await supabase.auth.getSession()
    
    if (error) {
      console.error('Session error:', error)
      // Clear invalid session
      await supabase.auth.signOut()
      return null
    }
    
    return data.session
  } catch (error) {
    console.error('Get session error:', error)
    return null
  }
}

// Token refresh with retry
async function refreshSession(maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const { data, error } = await supabase.auth.refreshSession()
      
      if (error) {
        if (attempt === maxRetries) {
          console.error('Session refresh failed after retries:', error)
          await supabase.auth.signOut()
          throw new Error('Session expired. Please sign in again.')
        }
        
        // Wait before retry
        await new Promise(resolve => setTimeout(resolve, 1000 * attempt))
        continue
      }
      
      return data.session
    } catch (error) {
      if (attempt === maxRetries) {
        throw error
      }
    }
  }
}

// Protected route handler
import { NextRequest, NextResponse } from 'next/server'

export async function middleware(request: NextRequest) {
  try {
    const session = await getSession()
    
    if (!session) {
      // Redirect to login
      return NextResponse.redirect(new URL('/login', request.url))
    }
    
    // Check if token is expiring soon
    const expiresAt = session.expires_at
    const now = Math.floor(Date.now() / 1000)
    
    if (expiresAt && expiresAt - now < 300) {  // Less than 5 minutes
      try {
        await refreshSession()
      } catch (error) {
        console.error('Token refresh failed:', error)
        return NextResponse.redirect(new URL('/login', request.url))
      }
    }
    
    return NextResponse.next()
  } catch (error) {
    console.error('Middleware error:', error)
    return NextResponse.redirect(new URL('/error', request.url))
  }
}

React Error Boundaries

typescriptreact_error_handling.tsx
// Error Boundary component
import React, { Component, ErrorInfo, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
  onError?: (error: Error, errorInfo: ErrorInfo) => void
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo)
    
    // Call custom error handler
    this.props.onError?.(error, errorInfo)
    
    // Log to error tracking service
    this.logErrorToService(error, errorInfo)
  }

  logErrorToService(error: Error, errorInfo: ErrorInfo) {
    // Send to Sentry, LogRocket, etc.
    console.error('Logging to error service:', {
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
    })
  }

  render() {
    if (this.state.hasError) {
      // Custom fallback UI
      if (this.props.fallback) {
        return this.props.fallback
      }
      
      // Default error UI
      return (
        <div className="min-h-screen flex items-center justify-center bg-gray-50">
          <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
            <div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
              <svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
              </svg>
            </div>
            <h3 className="mt-4 text-lg font-medium text-center text-gray-900">
              Something went wrong
            </h3>
            <p className="mt-2 text-sm text-center text-gray-600">
              {this.state.error?.message || 'An unexpected error occurred'}
            </p>
            <button
              onClick={() => this.setState({ hasError: false })}
              className="mt-6 w-full bg-blue-600 text-white rounded-md px-4 py-2 hover:bg-blue-700"
            >
              Try again
            </button>
          </div>
        </div>
      )
    }

    return this.props.children
  }
}

// Usage in app
import { ErrorBoundary } from '@/components/ErrorBoundary'

function App() {
  return (
    <ErrorBoundary
      onError={(error, errorInfo) => {
        // Custom error handling
        console.error('App error:', error)
      }}
    >
      <YourAppContent />
    </ErrorBoundary>
  )
}

// Component-level error handling
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'

function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const supabase = createClient()

  useEffect(() => {
    loadUsers()
  }, [])

  async function loadUsers() {
    try {
      setLoading(true)
      setError(null)
      
      const { data, error: queryError } = await supabase
        .from('users')
        .select('*')
      
      if (queryError) {
        throw new Error(`Failed to load users: ${queryError.message}`)
      }
      
      setUsers(data || [])
    } catch (err) {
      console.error('Load users error:', err)
      setError(err instanceof Error ? err.message : 'An error occurred')
    } finally {
      setLoading(false)
    }
  }

  // Loading state
  if (loading) {
    return <div>Loading users...</div>
  }

  // Error state
  if (error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded-md p-4">
        <h3 className="text-red-800 font-medium">Error loading users</h3>
        <p className="text-red-600 text-sm mt-1">{error}</p>
        <button
          onClick={loadUsers}
          className="mt-3 text-sm text-red-600 hover:text-red-700 underline"
        >
          Try again
        </button>
      </div>
    )
  }

  // Success state
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

Retry Logic and Resilience

typescriptretry_logic.ts
// Retry utility with exponential backoff
async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation()
    } catch (error) {
      if (attempt === maxRetries) {
        throw error
      }
      
      // Exponential backoff: 1s, 2s, 4s, 8s...
      const delay = baseDelay * Math.pow(2, attempt - 1)
      console.log(`Retry attempt ${attempt} after ${delay}ms`)
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
  
  throw new Error('Max retries exceeded')
}

// Usage
const data = await withRetry(
  async () => {
    const { data, error } = await supabase.from('users').select('*')
    if (error) throw error
    return data
  },
  3,  // Max 3 retries
  1000  // Start with 1s delay
)

// Retry only for specific errors
async function withConditionalRetry<T>(
  operation: () => Promise<T>,
  shouldRetry: (error: any) => boolean,
  maxRetries: number = 3
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation()
    } catch (error) {
      // Check if error is retryable
      if (!shouldRetry(error) || attempt === maxRetries) {
        throw error
      }
      
      const delay = 1000 * attempt
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
  
  throw new Error('Max retries exceeded')
}

// Usage: Retry only network errors
function isNetworkError(error: any): boolean {
  return (
    error.message?.includes('network') ||
    error.message?.includes('timeout') ||
    error.message?.includes('fetch')
  )
}

const result = await withConditionalRetry(
  async () => {
    const { data, error } = await supabase.from('posts').select('*')
    if (error) throw error
    return data
  },
  isNetworkError,
  3
)

// Circuit breaker pattern
class CircuitBreaker {
  private failures = 0
  private lastFailureTime = 0
  private state: 'closed' | 'open' | 'half-open' = 'closed'
  
  constructor(
    private threshold: number = 5,
    private timeout: number = 60000  // 1 minute
  ) {}

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailureTime > this.timeout) {
        this.state = 'half-open'
      } else {
        throw new Error('Circuit breaker is open')
      }
    }

    try {
      const result = await operation()
      this.onSuccess()
      return result
    } catch (error) {
      this.onFailure()
      throw error
    }
  }

  private onSuccess() {
    this.failures = 0
    this.state = 'closed'
  }

  private onFailure() {
    this.failures++
    this.lastFailureTime = Date.now()
    
    if (this.failures >= this.threshold) {
      this.state = 'open'
      console.error('Circuit breaker opened due to failures')
    }
  }
}

// Usage
const breaker = new CircuitBreaker(5, 60000)

const data = await breaker.execute(async () => {
  const { data, error } = await supabase.from('users').select('*')
  if (error) throw error
  return data
})

Error Logging and Debugging

typescripterror_logging.ts
// Structured error logging
interface ErrorLog {
  timestamp: string
  level: 'error' | 'warning' | 'info'
  message: string
  context?: Record<string, any>
  stack?: string
  userId?: string
}

class ErrorLogger {
  private supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  )

  async log(error: Error, context?: Record<string, any>) {
    const errorLog: ErrorLog = {
      timestamp: new Date().toISOString(),
      level: 'error',
      message: error.message,
      context,
      stack: error.stack,
      userId: await this.getCurrentUserId(),
    }

    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('Error logged:', errorLog)
    }

    // Store in database
    try {
      await this.supabase.from('error_logs').insert(errorLog)
    } catch (logError) {
      console.error('Failed to log error:', logError)
    }

    // Send to external service (Sentry, LogRocket, etc.)
    this.sendToExternalService(errorLog)
  }

  private async getCurrentUserId(): Promise<string | undefined> {
    try {
      const { data: { user } } = await this.supabase.auth.getUser()
      return user?.id
    } catch {
      return undefined
    }
  }

  private sendToExternalService(log: ErrorLog) {
    // Example: Send to your error tracking service
    if (process.env.SENTRY_DSN) {
      // Sentry.captureException(log)
    }
  }
}

export const errorLogger = new ErrorLogger()

// Usage in application
try {
  const { data, error } = await supabase.from('users').select('*')
  if (error) throw error
} catch (error) {
  await errorLogger.log(error as Error, {
    operation: 'fetch_users',
    component: 'UserList',
  })
  throw error
}

-- Error logs table
create table error_logs (
  id uuid default gen_random_uuid() primary key,
  timestamp timestamp with time zone default now(),
  level text not null,
  message text not null,
  context jsonb,
  stack text,
  user_id uuid references auth.users(id),
  created_at timestamp with time zone default now()
);

create index idx_error_logs_timestamp on error_logs(timestamp desc);
create index idx_error_logs_user on error_logs(user_id);
create index idx_error_logs_level on error_logs(level);

-- Query error patterns
select
  message,
  count(*) as occurrences,
  array_agg(distinct user_id) as affected_users
from error_logs
where timestamp > now() - interval '24 hours'
group by message
order by occurrences desc
limit 10;

// Debug helper
function debugSupabaseQuery(query: any) {
  console.group('Supabase Query Debug')
  console.log('Query:', query)
  
  query.then(({ data, error, count }: any) => {
    if (error) {
      console.error('Error:', error)
    } else {
      console.log('Data:', data)
      console.log('Count:', count)
    }
    console.groupEnd()
  })
  
  return query
}

// Usage
const result = await debugSupabaseQuery(
  supabase.from('users').select('*', { count: 'exact' })
)

Error Handling Best Practices

  • Always Check Errors: Check error property in Supabase responses before accessing data
  • User-Friendly Messages: Display friendly messages to users while logging technical details
  • Implement Retry Logic: Retry transient failures with exponential backoff
  • Use Error Boundaries: Catch React errors preventing entire app crashes
  • Log Errors Properly: Store errors with context enabling effective debugging
  • Handle Edge Cases: Check for null data, empty arrays, and undefined values
  • Provide Fallbacks: Show cached data or default states when errors occur
  • Monitor Errors: Set up alerts for error rate spikes catching issues early
  • Test Error Paths: Write tests covering error scenarios ensuring proper handling
  • Document Errors: Document common errors and solutions for team reference
Pro Tip: Use TypeScript for type safety catching errors at compile time. Implement structured logging with context for debugging. Create reusable error handling utilities. Test error scenarios regularly. Review monitoring practices and security best practices.

Common Debugging Techniques

  • RLS Policy Errors: Check policies in Supabase Dashboard, test with service role key, verify user context
  • Network Timeouts: Implement retry logic, check API rate limits, verify network connectivity
  • Silent Failures: Always log errors, check browser console, enable verbose logging in development
  • Type Errors: Use TypeScript generating types from database schema with Supabase CLI

Conclusion

Mastering error handling in Supabase applications ensures robust, user-friendly experiences through comprehensive error catching, meaningful messages, proper logging, and effective debugging enabling applications to handle failures gracefully. By understanding Supabase error types including authentication, database, storage, and network errors, implementing try-catch patterns with async/await checking both data and error properties, handling authentication errors gracefully with user-friendly messages, managing database query errors with context-aware responses, dealing with storage failures properly, implementing React Error Boundaries preventing crashes, creating error logging systems capturing details for debugging, using debugging tools and techniques, and building resilient applications with retry logic and circuit breakers, you build production-ready applications remaining stable and maintainable. Error handling advantages include improved user experience through graceful failures, reduced debugging time with proper logging, increased reliability through retry logic, better monitoring detecting issues early, and maintainable code through consistent patterns. Always check error properties before accessing data, display user-friendly messages while logging technical details, implement retry logic for transient failures, use Error Boundaries catching React errors, log errors with context, handle edge cases checking null values, provide fallbacks showing cached data, monitor error rates, test error paths, and document common errors. Error handling demonstrates production-ready development ensuring applications remain stable throughout their lifecycle. Continue exploring monitoring strategies and Edge Functions.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.