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 Category | Common Causes | HTTP Status |
|---|---|---|
| Authentication | Invalid credentials, expired token, missing session | 401, 403 |
| Database | RLS policy violation, constraint violation, invalid query | 400, 403 |
| Storage | File size limit, invalid bucket, RLS policy | 400, 413 |
| Network | Timeout, connection failed, DNS error | 408, 502, 503 |
| Validation | Invalid data format, missing fields, type mismatch | 400 |
| Rate Limiting | Too many requests | 429 |
| Server | Internal server error, database down | 500, 503 |
Basic Error Handling Patterns
// 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
// 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
// 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
// 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
// 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
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.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


