Skip to content

Error Handling

Robust error handling is crucial for building reliable applications with OstrichDB. This guide covers all error types, handling strategies, and best practices for managing errors in the SDK.

The primary error type thrown by the SDK, providing detailed information about API failures:

import { OstrichDBError } from 'ostrichdb-js';
interface OstrichDBError extends Error {
statusCode: number; // HTTP status code
response: any; // Full response object
message: string; // Error message
name: 'OstrichDBError';
}
try {
await db.create_project('my-project');
} catch (error) {
if (error instanceof OstrichDBError) {
console.log('Status Code:', error.statusCode);
console.log('Error Message:', error.message);
console.log('Full Response:', error.response);
}
}

Invalid request parameters or malformed data.

try {
// Invalid data type
await db.create_record('project', 'collection', 'cluster', 'record', 'INVALID_TYPE', 'value');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 400) {
console.error('Invalid request parameters:', error.message);
// Handle validation errors
}
}

Missing or invalid authentication token.

try {
await db.list_projects();
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 401) {
console.error('Authentication required');
// Redirect to login or refresh token
redirectToLogin();
}
}

Valid authentication but insufficient permissions.

try {
await db.delete_project('restricted-project');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 403) {
console.error('Access denied:', error.message);
// Show permission denied message
showAccessDeniedDialog();
}
}

Requested resource doesn’t exist.

try {
const record = await db.get_record('project', 'collection', 'cluster', 'nonexistent');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 404) {
console.log('Record not found');
// Handle missing resource gracefully
return null;
}
}

Resource already exists when trying to create.

try {
await db.create_project('existing-project');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 409) {
console.log('Project already exists');
// Use existing project or suggest different name
return handleExistingProject();
}
}

Rate limit exceeded.

try {
await db.list_records('project', 'collection', 'cluster');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 429) {
console.error('Rate limit exceeded');
// Implement exponential backoff
await retryWithBackoff(() => db.list_records('project', 'collection', 'cluster'));
}
}

Server-side error occurred.

try {
await db.create_collection('project', 'collection');
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 500) {
console.error('Server error:', error.message);
// Log error and show user-friendly message
logErrorToService(error);
showServerErrorMessage();
}
}

Server temporarily unavailable.

try {
await db.health_check();
} catch (error) {
if (error instanceof OstrichDBError && error.statusCode === 503) {
console.error('Service temporarily unavailable');
// Implement retry logic
await retryAfterDelay(() => db.health_check(), 5000);
}
}
async function basicErrorHandling() {
try {
const projects = await db.list_projects();
return projects;
} catch (error) {
if (error instanceof OstrichDBError) {
console.error(`API Error (${error.statusCode}): ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
throw error; // Re-throw if needed
}
}
utils/errorHandler.ts
export class DatabaseErrorHandler {
static handle(error: unknown, context?: string): void {
const contextPrefix = context ? `[${context}] ` : '';
if (error instanceof OstrichDBError) {
switch (error.statusCode) {
case 400:
console.error(`${contextPrefix}Bad Request: ${error.message}`);
break;
case 401:
console.error(`${contextPrefix}Unauthorized - redirecting to login`);
window.location.href = '/login';
break;
case 403:
console.error(`${contextPrefix}Forbidden: ${error.message}`);
this.showAccessDeniedNotification();
break;
case 404:
console.warn(`${contextPrefix}Resource not found: ${error.message}`);
break;
case 409:
console.warn(`${contextPrefix}Resource conflict: ${error.message}`);
break;
case 429:
console.warn(`${contextPrefix}Rate limit exceeded`);
this.handleRateLimit();
break;
case 500:
console.error(`${contextPrefix}Server error: ${error.message}`);
this.logServerError(error);
break;
default:
console.error(`${contextPrefix}API Error (${error.statusCode}): ${error.message}`);
}
} else if (error instanceof Error) {
console.error(`${contextPrefix}Unexpected error: ${error.message}`);
} else {
console.error(`${contextPrefix}Unknown error:`, error);
}
}
private static showAccessDeniedNotification() {
// Implementation depends on your UI framework
console.log('Access denied - showing notification');
}
private static handleRateLimit() {
// Implement rate limit handling
console.log('Rate limit exceeded - implementing backoff');
}
private static logServerError(error: OstrichDBError) {
// Log to external service
console.log('Logging server error to monitoring service');
}
}
// Usage
try {
await db.create_project('test-project');
} catch (error) {
DatabaseErrorHandler.handle(error, 'Project Creation');
}
interface RetryOptions {
maxRetries: number;
baseDelay: number;
maxDelay: number;
retryCondition?: (error: any) => boolean;
}
async function withRetry<T>(
operation: () => Promise<T>,
options: RetryOptions
): Promise<T> {
const { maxRetries, baseDelay, maxDelay, retryCondition } = options;
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
// Don't retry if we've reached max attempts
if (attempt === maxRetries) {
break;
}
// Check if error is retryable
if (retryCondition && !retryCondition(error)) {
break;
}
// Default retry condition for OstrichDB errors
if (error instanceof OstrichDBError) {
const isRetryable = error.statusCode >= 500 || error.statusCode === 429;
if (!isRetryable) {
break;
}
}
// Calculate delay with exponential backoff
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage examples
async function createProjectWithRetry(projectName: string) {
return withRetry(
() => db.create_project(projectName),
{
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
retryCondition: (error) => {
// Only retry on server errors and rate limits
return error instanceof OstrichDBError &&
(error.statusCode >= 500 || error.statusCode === 429);
}
}
);
}
async function fetchRecordsWithRetry(project: string, collection: string, cluster: string) {
return withRetry(
() => db.list_records(project, collection, cluster),
{
maxRetries: 5,
baseDelay: 500,
maxDelay: 5000
}
);
}
class CircuitBreaker {
private failures = 0;
private lastFailTime = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold: number = 5,
private timeout: number = 60000,
private resetTimeout: number = 30000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailTime < this.resetTimeout) {
throw new Error('Circuit breaker is open');
}
this.state = 'half-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.lastFailTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
getState() {
return this.state;
}
}
// Usage
const circuitBreaker = new CircuitBreaker(3, 60000, 30000);
async function robustDatabaseOperation() {
try {
return await circuitBreaker.execute(() => db.list_projects());
} catch (error) {
if (error.message === 'Circuit breaker is open') {
console.log('Service temporarily unavailable');
return getCachedProjects(); // Fallback to cache
}
throw error;
}
}
middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { OstrichDBError } from 'ostrichdb-js';
export function databaseErrorHandler(
error: any,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof OstrichDBError) {
const statusCode = error.statusCode;
const message = getErrorMessage(statusCode, error.message);
console.error(`Database Error [${req.method} ${req.path}]:`, {
statusCode,
message: error.message,
response: error.response
});
res.status(statusCode).json({
error: message,
statusCode,
timestamp: new Date().toISOString(),
path: req.path
});
} else {
next(error);
}
}
function getErrorMessage(statusCode: number, originalMessage: string): string {
switch (statusCode) {
case 400:
return 'Invalid request parameters';
case 401:
return 'Authentication required';
case 403:
return 'Access denied';
case 404:
return 'Resource not found';
case 409:
return 'Resource already exists';
case 429:
return 'Too many requests';
case 500:
return 'Internal server error';
case 503:
return 'Service temporarily unavailable';
default:
return originalMessage;
}
}
// Usage in Express app
app.use('/api', apiRoutes);
app.use(databaseErrorHandler);
utils/apiErrorHandler.ts
import { NextResponse } from 'next/server';
import { OstrichDBError } from 'ostrichdb-js';
export function handleDatabaseError(error: unknown, context?: string) {
console.error(`Database error${context ? ` (${context})` : ''}:`, error);
if (error instanceof OstrichDBError) {
const statusCode = error.statusCode;
const message = getClientSafeMessage(statusCode);
return NextResponse.json(
{
error: message,
statusCode,
timestamp: new Date().toISOString()
},
{ status: statusCode }
);
}
// Generic error
return NextResponse.json(
{
error: 'Internal server error',
statusCode: 500,
timestamp: new Date().toISOString()
},
{ status: 500 }
);
}
function getClientSafeMessage(statusCode: number): string {
switch (statusCode) {
case 400:
return 'Invalid request data';
case 401:
return 'Authentication required';
case 403:
return 'Access denied';
case 404:
return 'Resource not found';
case 409:
return 'Resource already exists';
case 429:
return 'Rate limit exceeded';
default:
return 'Server error occurred';
}
}
// Usage in API route
export async function POST(request: NextRequest) {
try {
const db = new OstrichDB({ /* config */ });
await db.create_project('test');
return NextResponse.json({ success: true });
} catch (error) {
return handleDatabaseError(error, 'Create Project');
}
}
components/DatabaseErrorBoundary.tsx
import React, { Component, ReactNode } from 'react';
import { OstrichDBError } from 'ostrichdb-js';
interface Props {
children: ReactNode;
fallback?: (error: Error) => ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class DatabaseErrorBoundary 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: any) {
console.error('Database error caught by boundary:', error, errorInfo);
if (error instanceof OstrichDBError) {
// Log database errors to monitoring service
this.logDatabaseError(error);
}
}
private logDatabaseError(error: OstrichDBError) {
// Implementation depends on your logging service
console.log('Logging database error:', {
statusCode: error.statusCode,
message: error.message,
response: error.response
});
}
render() {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) {
return this.props.fallback(this.state.error);
}
if (this.state.error instanceof OstrichDBError) {
return this.renderDatabaseError(this.state.error);
}
return this.renderGenericError();
}
return this.props.children;
}
private renderDatabaseError(error: OstrichDBError) {
const message = this.getDatabaseErrorMessage(error.statusCode);
return (
<div className="error-boundary database-error">
<h2>Database Error</h2>
<p>{message}</p>
{error.statusCode === 401 && (
<button onClick={() => window.location.href = '/login'}>
Go to Login
</button>
)}
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
private getDatabaseErrorMessage(statusCode: number): string {
switch (statusCode) {
case 401:
return 'Please log in to continue.';
case 403:
return 'You don\'t have permission to access this resource.';
case 404:
return 'The requested data was not found.';
case 429:
return 'Too many requests. Please try again later.';
case 500:
return 'A server error occurred. Please try again.';
default:
return 'A database error occurred. Please try again.';
}
}
private renderGenericError() {
return (
<div className="error-boundary generic-error">
<h2>Something went wrong</h2>
<p>An unexpected error occurred. Please try again.</p>
<button onClick={() => this.setState({ hasError: false })}>
Try Again
</button>
</div>
);
}
}
// Usage
function App() {
return (
<DatabaseErrorBoundary>
<UserDashboard />
</DatabaseErrorBoundary>
);
}
utils/errorClassification.ts
import { OstrichDBError } from 'ostrichdb-js';
export class ErrorClassifier {
static isRetryable(error: unknown): boolean {
if (error instanceof OstrichDBError) {
// Retry on server errors and rate limits
return error.statusCode >= 500 || error.statusCode === 429;
}
// Retry on network errors
if (error instanceof Error) {
return error.message.includes('network') ||
error.message.includes('timeout') ||
error.message.includes('ECONNRESET');
}
return false;
}
static isAuthenticationError(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode === 401;
}
static isAuthorizationError(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode === 403;
}
static isResourceNotFound(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode === 404;
}
static isResourceConflict(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode === 409;
}
static isRateLimit(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode === 429;
}
static isServerError(error: unknown): boolean {
return error instanceof OstrichDBError && error.statusCode >= 500;
}
static getErrorCategory(error: unknown): string {
if (this.isAuthenticationError(error)) return 'authentication';
if (this.isAuthorizationError(error)) return 'authorization';
if (this.isResourceNotFound(error)) return 'not_found';
if (this.isResourceConflict(error)) return 'conflict';
if (this.isRateLimit(error)) return 'rate_limit';
if (this.isServerError(error)) return 'server_error';
if (this.isRetryable(error)) return 'retryable';
return 'unknown';
}
}
services/errorLogger.ts
import { OstrichDBError } from 'ostrichdb-js';
interface ErrorLogEntry {
timestamp: string;
level: 'error' | 'warn' | 'info';
category: string;
message: string;
statusCode?: number;
response?: any;
context?: string;
userId?: string;
}
export class ErrorLogger {
private logs: ErrorLogEntry[] = [];
log(error: unknown, context?: string, userId?: string) {
const entry: ErrorLogEntry = {
timestamp: new Date().toISOString(),
level: this.getLogLevel(error),
category: ErrorClassifier.getErrorCategory(error),
message: this.getErrorMessage(error),
context,
userId
};
if (error instanceof OstrichDBError) {
entry.statusCode = error.statusCode;
entry.response = error.response;
}
this.logs.push(entry);
this.writeLog(entry);
// Send to external service in production
if (process.env.NODE_ENV === 'production') {
this.sendToLoggingService(entry);
}
}
private getLogLevel(error: unknown): 'error' | 'warn' | 'info' {
if (error instanceof OstrichDBError) {
if (error.statusCode >= 500) return 'error';
if (error.statusCode === 404 || error.statusCode === 409) return 'info';
return 'warn';
}
return 'error';
}
private getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
private writeLog(entry: ErrorLogEntry) {
const logMessage = `[${entry.timestamp}] ${entry.level.toUpperCase()}: ${entry.message}`;
switch (entry.level) {
case 'error':
console.error(logMessage, entry);
break;
case 'warn':
console.warn(logMessage, entry);
break;
case 'info':
console.info(logMessage, entry);
break;
}
}
private async sendToLoggingService(entry: ErrorLogEntry) {
// Implementation depends on your logging service
// Example: Sentry, LogRocket, DataDog, etc.
try {
await fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
} catch (logError) {
console.error('Failed to send log to service:', logError);
}
}
getLogs(): ErrorLogEntry[] {
return [...this.logs];
}
clearLogs() {
this.logs = [];
}
}
// Global instance
export const errorLogger = new ErrorLogger();
// Usage
try {
await db.create_project('test');
} catch (error) {
errorLogger.log(error, 'Project Creation', 'user-123');
throw error;
}
async function getUsersWithFallback(): Promise<User[]> {
try {
const users = await db.project('app').collection('users').cluster('active').listRecords();
return parseUsers(users);
} catch (error) {
if (ErrorClassifier.isServerError(error)) {
console.warn('Database unavailable, using cached data');
return getCachedUsers();
}
if (ErrorClassifier.isResourceNotFound(error)) {
console.info('No users found, returning empty array');
return [];
}
throw error; // Re-throw unexpected errors
}
}
function getUserFriendlyMessage(error: unknown): string {
if (error instanceof OstrichDBError) {
switch (error.statusCode) {
case 400:
return 'Please check your input and try again.';
case 401:
return 'Please log in to continue.';
case 403:
return 'You don\'t have permission to perform this action.';
case 404:
return 'The requested item was not found.';
case 409:
return 'This item already exists. Please choose a different name.';
case 429:
return 'Too many requests. Please wait a moment and try again.';
case 500:
return 'A server error occurred. Please try again later.';
default:
return 'An unexpected error occurred. Please try again.';
}
}
return 'Something went wrong. Please try again.';
}
async function createProjectWithProgression(name: string): Promise<string> {
try {
// Try to create project
await db.create_project(name);
return 'Project created successfully';
} catch (error) {
if (ErrorClassifier.isResourceConflict(error)) {
// Project exists, try to use it
try {
const projects = await db.list_projects();
const existingProject = projects.find(p => p.name === name);
if (existingProject) {
return 'Using existing project';
}
} catch (listError) {
// Ignore list error, fall through to main error handling
}
}
// Handle other errors or re-throw
throw error;
}
}
class DatabaseMonitor {
private errorCounts = new Map<number, number>();
private lastAlertTime = 0;
private alertThreshold = 10;
private alertCooldown = 300000; // 5 minutes
trackError(error: OstrichDBError) {
const statusCode = error.statusCode;
const count = this.errorCounts.get(statusCode) || 0;
this.errorCounts.set(statusCode, count + 1);
// Check if we should send an alert
if (count + 1 >= this.alertThreshold &&
Date.now() - this.lastAlertTime > this.alertCooldown) {
this.sendAlert(statusCode, count + 1);
this.lastAlertTime = Date.now();
}
}
private async sendAlert(statusCode: number, count: number) {
const message = `High error rate detected: ${count} errors with status ${statusCode}`;
console.error('ALERT:', message);
// Send to monitoring service
try {
await fetch('/api/alerts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'database_error_spike',
statusCode,
count,
timestamp: new Date().toISOString()
})
});
} catch (alertError) {
console.error('Failed to send alert:', alertError);
}
}
getErrorStats() {
return Object.fromEntries(this.errorCounts);
}
reset() {
this.errorCounts.clear();
}
}
// Global monitor
export const databaseMonitor = new DatabaseMonitor();
// Use in error handling
try {
await db.create_project('test');
} catch (error) {
if (error instanceof OstrichDBError) {
databaseMonitor.trackError(error);
}
throw error;
}
tests/errorHandling.test.ts
import { OstrichDBError } from 'ostrichdb-js';
describe('Error Handling', () => {
test('should handle authentication error', async () => {
const mockError = new OstrichDBError('Unauthorized', 401, {});
jest.spyOn(db, 'list_projects').mockRejectedValue(mockError);
try {
await getUserProjects();
fail('Should have thrown error');
} catch (error) {
expect(error).toBeInstanceOf(OstrichDBError);
expect(error.statusCode).toBe(401);
}
});
test('should retry on server error', async () => {
const serverError = new OstrichDBError('Internal Server Error', 500, {});
jest.spyOn(db, 'create_project')
.mockRejectedValueOnce(serverError)
.mockRejectedValueOnce(serverError)
.mockResolvedValueOnce(undefined);
await expect(createProjectWithRetry('test')).resolves.toBeUndefined();
expect(db.create_project).toHaveBeenCalledTimes(3);
});
});

Mastering error handling is crucial for robust applications:

  1. Configuration - Set up proper timeouts and retry policies
  2. Framework Integration - Apply error handling in your specific framework
  3. API Reference - Understand all possible error scenarios
  4. Builder Pattern - Use chainable operations with proper error handling

Comprehensive error handling ensures your application remains stable and provides excellent user experience even when things go wrong.