Error Handling
The Volcano SDK returns error objects instead of throwing, so you can inspect and handle failures consistently across every operation.
The Volcano SDK uses a consistent error handling pattern across all operations. Rather than throwing exceptions, methods return error objects that you can inspect and handle appropriately.
The Error Pattern
All SDK methods return an object with an error property:
const { data, error } = await volcano.from('posts').select('*');
if (error) {
console.error('Something went wrong:', error.message);
return;
}
// data is safe to use
console.log(data);This pattern has several advantages:
- Explicit error handling - You must acknowledge the error property
- No try/catch needed - Errors don't interrupt execution flow
- Consistent API - Same pattern for auth, database, storage, and functions
- Type safety - TypeScript knows the data type when error is checked
Authentication Errors
Sign Up
const { confirmationRequired, message, error } = await volcano.auth.signUp({
email: 'user@example.com',
password: 'weak',
});
if (error) {
switch (true) {
case error.message.includes('already exists'):
showError('An account with this email already exists. Try signing in.');
break;
case error.message.includes('weak password'):
showError('Please choose a stronger password with at least 8 characters.');
break;
case error.message.includes('invalid email'):
showError('Please enter a valid email address.');
break;
default:
showError('Sign up failed. Please try again.');
console.error('Sign up error:', error);
}
return;
}
// Success — sign up is session-less; sign in next to authenticate.
console.log(message ?? 'Account created. You can now sign in.');Sign In
const { user, session, error } = await volcano.auth.signIn({
email: 'user@example.com',
password: 'password123',
});
if (error) {
switch (true) {
case error.message.includes('Invalid credentials'):
showError('Incorrect email or password.');
break;
case error.message.includes('email not confirmed'):
showError('Please confirm your email before signing in.');
break;
case error.message.includes('too many attempts'):
showError('Too many failed attempts. Please try again later.');
break;
default:
showError('Sign in failed. Please try again.');
console.error('Sign in error:', error);
}
return;
}Session Errors
const { user, error } = await volcano.auth.getUser();
if (error) {
if (error.message.includes('No active session')) {
// User is not logged in
redirectToLogin();
return;
}
if (error.message.includes('Session expired')) {
// Session needs refresh (SDK usually handles this automatically)
const { session, error: refreshError } = await volcano.auth.refreshSession();
if (refreshError) {
redirectToLogin();
return;
}
// Retry the original request
}
}Database Errors
Query Errors
const { data, error } = await volcano.from('posts').select('*').eq('status', 'published');
if (error) {
switch (true) {
case error.message.includes('No active session'):
showError('Please sign in to view posts.');
break;
case error.message.includes('Database name not set'):
console.error('Developer error: Call volcano.database() first');
break;
case error.message.includes('column') && error.message.includes('does not exist'):
console.error('Developer error: Invalid column name in query');
break;
case error.message.includes('permission denied'):
showError("You don't have permission to view this data.");
break;
case error.message.includes('timeout'):
showError('Request timed out. Please try again.');
break;
default:
showError('Failed to load data.');
console.error('Query error:', error);
}
return;
}Insert Errors
const { data, error } = await volcano.insert('posts', {
title: 'My Post',
content: 'Content here',
});
if (error) {
switch (true) {
case error.message.includes('violates unique constraint'):
showError('A post with this title already exists.');
break;
case error.message.includes('violates foreign key'):
showError('Invalid reference. Please check your data.');
break;
case error.message.includes('violates check constraint'):
showError('Invalid data. Please check your input.');
break;
case error.message.includes('permission denied'):
showError("You don't have permission to create posts.");
break;
default:
showError('Failed to create post.');
console.error('Insert error:', error);
}
return;
}Update/Delete Errors
const { data, error } = await volcano.update('posts', { status: 'published' }).eq('id', postId);
if (error) {
if (error.message.includes('permission denied')) {
showError('You can only edit your own posts.');
return;
}
showError('Failed to update post.');
return;
}
// Check if any rows were updated
if (!data || data.length === 0) {
showError('Post not found or already deleted.');
return;
}Storage Errors
const { data, error } = await volcano.storage.from('uploads').upload('documents/report.pdf', file);
if (error) {
switch (true) {
case error.message.includes('No active session'):
showError('Please sign in to upload files.');
break;
case error.message.includes('Bucket not found'):
console.error('Developer error: Invalid bucket name');
break;
case error.message.includes('File too large'):
showError('File is too large. Maximum size is 100MB.');
break;
case error.message.includes('permission denied'):
showError("You don't have permission to upload to this location.");
break;
case error.message.includes('invalid file type'):
showError('This file type is not allowed.');
break;
default:
showError('Upload failed. Please try again.');
console.error('Upload error:', error);
}
return;
}Download Errors
const { data: blob, error } = await volcano.storage
.from('uploads')
.download('documents/report.pdf');
if (error) {
switch (true) {
case error.message.includes('File not found'):
case error.message.includes('404'):
showError('File not found. It may have been deleted.');
break;
case error.message.includes('permission denied'):
showError("You don't have permission to download this file.");
break;
default:
showError('Download failed. Please try again.');
}
return;
}Function Errors
const { data, error } = await volcano.functions.invoke('process-payment', {
amount: 1999,
currency: 'usd',
});
if (error) {
// Platform-layer failures are a VolcanoSystemError (error.isSystemError ===
// true): the call never reached your function — deploy down/failed, gateway
// error, rate limit, timeout, or network failure. error.status is the HTTP
// status (null for transport failures).
if (error.isSystemError) {
switch (true) {
case error.status === 429:
showError('Too many requests. Please wait a moment.');
break;
case error.status === null:
// Transport failure (timeout, DNS, offline) — no HTTP status.
showError('Network problem. Please check your connection and try again.');
break;
default:
// Failed/provisioning deploy or gateway error (e.g. 400/503).
showError('The service is temporarily unavailable. Please try again shortly.');
}
console.error('Platform error:', error.status, error.message);
return;
}
// Pre-flight / caller errors stay plain Error (message is case-sensitive).
switch (true) {
case error.message.includes('No active session'):
showError('Please sign in to continue.');
break;
case error.message.includes('function not found'):
console.error('Developer error: Invalid function name');
break;
default:
showError('Operation failed. Please try again.');
console.error('Function error:', error);
}
return;
}
// A running function's own error comes back in the response body, not `error`.
if (data && data.error) {
showError(data.error);
return;
}Realtime Errors
Connection Errors
const realtime = new VolcanoRealtime({ ... });
realtime.onError((ctx) => {
console.error('Connection error:', ctx.message);
if (ctx.message?.includes('authentication')) {
// Token may have expired
refreshTokenAndReconnect();
} else if (ctx.message?.includes('network')) {
showError('Connection lost. Reconnecting...');
}
});
realtime.onDisconnect((ctx) => {
if (!ctx.reconnect) {
// Won't auto-reconnect, handle manually
showError('Connection closed.');
}
});Subscription Errors
const channel = realtime.channel('posts', { type: 'postgres' });
try {
await channel.subscribe();
} catch (error) {
console.error('Subscription failed:', error.message);
showError('Failed to subscribe to updates.');
}Creating Custom Error Handlers
Centralized Error Handler
// lib/errorHandler.js
export function handleApiError(error, context = 'Operation') {
// Log for debugging
console.error(`${context} error:`, error);
// Common errors
if (error.message.includes('No active session')) {
return {
message: 'Please sign in to continue.',
action: 'redirect_login',
};
}
if (error.message.includes('permission denied')) {
return {
message: "You don't have permission to perform this action.",
action: 'show_error',
};
}
if (error.message.includes('timeout')) {
return {
message: 'Request timed out. Please try again.',
action: 'retry',
};
}
if (error.message.includes('rate limit')) {
return {
message: 'Too many requests. Please wait a moment.',
action: 'wait',
};
}
// Default
return {
message: `${context} failed. Please try again.`,
action: 'show_error',
};
}
// Usage
const { data, error } = await volcano.from('posts').select('*');
if (error) {
const { message, action } = handleApiError(error, 'Loading posts');
switch (action) {
case 'redirect_login':
router.push('/login');
break;
case 'retry':
// Implement retry logic
break;
default:
showToast(message, 'error');
}
return;
}React Error Hook
// hooks/useApiCall.ts
import { useState, useCallback } from 'react';
interface ApiCallState<T> {
data: T | null;
error: Error | null;
loading: boolean;
}
export function useApiCall<T>() {
const [state, setState] = useState<ApiCallState<T>>({
data: null,
error: null,
loading: false,
});
const execute = useCallback(async (
apiCall: () => Promise<{ data: T | null; error: Error | null }>
) => {
setState({ data: null, error: null, loading: true });
try {
const result = await apiCall();
if (result.error) {
setState({ data: null, error: result.error, loading: false });
return { data: null, error: result.error };
}
setState({ data: result.data, error: null, loading: false });
return { data: result.data, error: null };
} catch (e) {
const error = e instanceof Error ? e : new Error('Unknown error');
setState({ data: null, error, loading: false });
return { data: null, error };
}
}, []);
return { ...state, execute };
}
// Usage
function PostList() {
const { data: posts, error, loading, execute } = useApiCall<Post[]>();
useEffect(() => {
execute(() =>
volcano
.from<Post>('posts')
.select('*')
.order('created_at', { ascending: false })
);
}, [execute]);
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
if (!posts) return <Empty />;
return <PostGrid posts={posts} />;
}Retry Strategies
Simple Retry
async function fetchWithRetry(fn, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const { data, error } = await fn();
if (!error) {
return { data, error: null };
}
lastError = error;
// Don't retry certain errors
if (
error.message.includes('permission denied') ||
error.message.includes('invalid') ||
error.message.includes('not found')
) {
return { data: null, error };
}
// Wait before retrying (exponential backoff)
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
return { data: null, error: lastError };
}
// Usage
const { data, error } = await fetchWithRetry(() => volcano.from('posts').select('*'));Retry with Toast Notification
async function fetchWithProgress(fn, options = {}) {
const { maxRetries = 3, context = 'Loading' } = options;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const { data, error } = await fn();
if (!error) {
return { data, error: null };
}
if (attempt < maxRetries && error.message.includes('timeout')) {
showToast(`${context} is taking longer than expected. Retrying...`, 'info');
await new Promise((resolve) => setTimeout(resolve, 2000));
continue;
}
return { data: null, error };
}
}Best Practices
1. Always Check Errors
// Good
const { data, error } = await volcano.from('posts').select('*');
if (error) {
handleError(error);
return;
}
// Use data safely
// Avoid
const { data } = await volcano.from('posts').select('*');
// data could be null if there was an error2. Provide Meaningful Messages
// Good
if (error.message.includes('permission denied')) {
showError('You can only view your own posts.');
}
// Avoid
if (error) {
showError(error.message); // May expose technical details
}3. Log for Debugging
if (error) {
// Log the full error for debugging
console.error('Failed to load posts:', error);
// Show a user-friendly message
showError('Unable to load posts. Please try again.');
}4. Handle Network Issues
const { data, error } = await volcano.from('posts').select('*');
if (error) {
if (!navigator.onLine) {
showError('You appear to be offline. Please check your connection.');
} else if (error.message.includes('timeout')) {
showError('Connection is slow. Please try again.');
} else {
showError('Something went wrong. Please try again.');
}
}5. Clean Up on Error
setLoading(true);
setError(null);
const { data, error } = await volcano.from('posts').select('*');
setLoading(false);
if (error) {
setError(error.message);
setData(null); // Clear stale data
return;
}
setData(data);Next Steps
- Authentication - Auth-specific error handling
- Database - Query error patterns
- TypeScript - Type-safe error handling