Error Handling Best Practices
Robust error handling ensures your application remains stable and provides good user experience even when things go wrong. This guide covers patterns for handling errors during initialization, data operations, and queries.
Error Handling Principles
Effective error handling follows these principles:
- Anticipate failures - Assume network requests and data operations can fail
- Fail gracefully - Provide fallbacks and meaningful error messages
- Log for debugging - Capture enough context to diagnose issues
- Recover when possible - Retry transient failures automatically
Initialization Errors
Wrap Init in Try-Catch
Always wrap initialization in error handling:
async function initializeDcupl(): Promise<Dcupl | null> {
const dcupl = new Dcupl();
try {
dcupl.models.set(productModel);
dcupl.models.set(categoryModel);
const products = await fetchProducts();
dcupl.data.set(products, { model: 'Product' });
await dcupl.init();
return dcupl;
} catch (error) {
console.error('Failed to initialize dcupl:', error);
return null;
}
}
// Usage
const dcupl = await initializeDcupl();
if (!dcupl) {
showErrorMessage('Failed to load data. Please refresh the page.');
}Handle Model Definition Errors
Invalid model definitions throw errors. Validate during development:
function setModelSafely(dcupl: Dcupl, model: ModelDefinition): boolean {
try {
dcupl.models.set(model);
return true;
} catch (error) {
console.error(`Invalid model definition for ${model.key}:`, error);
return false;
}
}
// Usage
const isValid = setModelSafely(dcupl, productModel);
if (!isValid) {
// Handle invalid model - likely a development error
throw new Error('Model validation failed. Check model definitions.');
}Retry Failed Data Loading
Network requests can fail. Implement retry logic:
async function loadDataWithRetry(
dcupl: Dcupl,
fetchFn: () => Promise<any[]>,
model: string,
maxRetries = 3
): Promise<boolean> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await fetchFn();
dcupl.data.set(data, { model });
return true;
} catch (error) {
console.error(`Load attempt ${attempt} failed for ${model}:`, error);
if (attempt === maxRetries) {
return false;
}
// Exponential backoff: 1s, 2s, 4s
const delay = 1000 * Math.pow(2, attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
return false;
}
// Usage
const loaded = await loadDataWithRetry(
dcupl,
() => fetch('/api/products').then((r) => r.json()),
'Product'
);
if (!loaded) {
showErrorMessage('Unable to load products. Please check your connection.');
}Data Operation Errors
Handle Update Failures
Updates can fail if data is invalid or keys do not exist:
function updateProductPrice(
dcupl: Dcupl,
productKey: string,
newPrice: number
): { success: boolean; error?: string } {
try {
// Validate input
if (newPrice < 0) {
return { success: false, error: 'Price cannot be negative' };
}
// Check if product exists
const list = dcupl.lists.create({ modelKey: 'Product' });
const product = list.catalog.getItemByKey(productKey);
list.destroy();
if (!product) {
return { success: false, error: `Product ${productKey} not found` };
}
// Perform update
dcupl.data.update([
{ key: productKey, price: newPrice },
], { model: 'Product' });
return { success: true };
} catch (error) {
console.error('Update failed:', error);
return { success: false, error: 'Failed to update product' };
}
}
// Usage
const result = updateProductPrice(dcupl, 'p1', 99.99);
if (!result.success) {
showErrorMessage(result.error!);
}Batch Update Error Handling
When updating multiple items, track which succeeded and which failed:
interface BatchResult {
succeeded: string[];
failed: Array<{ key: string; error: string }>;
}
function batchUpdatePrices(
dcupl: Dcupl,
updates: Array<{ key: string; price: number }>
): BatchResult {
const result: BatchResult = { succeeded: [], failed: [] };
// Validate all updates first
const validUpdates: Array<{ key: string; price: number }> = [];
for (const update of updates) {
if (update.price < 0) {
result.failed.push({
key: update.key,
error: 'Price cannot be negative',
});
continue;
}
validUpdates.push(update);
}
// Perform valid updates
if (validUpdates.length > 0) {
try {
dcupl.data.update(validUpdates, { model: 'Product' });
result.succeeded = validUpdates.map((u) => u.key);
} catch (error) {
// All updates failed
for (const update of validUpdates) {
result.failed.push({
key: update.key,
error: 'Update operation failed',
});
}
}
}
return result;
}
// Usage
const result = batchUpdatePrices(dcupl, [
{ key: 'p1', price: 99.99 },
{ key: 'p2', price: -10 }, // Invalid
{ key: 'p3', price: 149.99 },
]);
console.log(`Updated: ${result.succeeded.length}, Failed: ${result.failed.length}`);
for (const failure of result.failed) {
console.warn(`Failed to update ${failure.key}: ${failure.error}`);
}Handle Delete Errors
Deleting non-existent keys is usually safe, but wrap for consistency:
function deleteProducts(dcupl: Dcupl, keys: string[]): { success: boolean; error?: string } {
try {
if (keys.length === 0) {
return { success: true };
}
dcupl.data.remove(
keys.map((key) => ({ key })),
{ model: 'Product' }
);
return { success: true };
} catch (error) {
console.error('Delete failed:', error);
return { success: false, error: 'Failed to delete products' };
}
}Query Error Handling
Handle Missing Data Gracefully
Queries for non-existent items should not crash your app:
function getProductByKey(dcupl: Dcupl, productKey: string): Product | null {
try {
const list = dcupl.lists.create({ modelKey: 'Product' });
const product = list.catalog.getItemByKey(productKey);
list.destroy();
return product || null;
} catch (error) {
console.error('Failed to get product:', error);
return null;
}
}
// Usage in UI
const product = getProductByKey(dcupl, productId);
if (product) {
renderProductDetails(product);
} else {
renderProductNotFound();
}Validate Query Parameters
Validate user-provided query parameters before executing:
interface ProductFilters {
category?: string;
minPrice?: number;
maxPrice?: number;
}
function validateFilters(filters: ProductFilters): string | null {
if (filters.minPrice !== undefined && filters.minPrice < 0) {
return 'Minimum price cannot be negative';
}
if (filters.maxPrice !== undefined && filters.maxPrice < 0) {
return 'Maximum price cannot be negative';
}
if (
filters.minPrice !== undefined &&
filters.maxPrice !== undefined &&
filters.minPrice > filters.maxPrice
) {
return 'Minimum price cannot be greater than maximum price';
}
return null;
}
function queryProducts(
list: DcuplList,
filters: ProductFilters
): { items: Product[]; error?: string } {
// Validate first
const validationError = validateFilters(filters);
if (validationError) {
return { items: [], error: validationError };
}
try {
list.catalog.query.clear();
if (filters.category) {
list.catalog.query.addCondition({
attribute: 'category',
operator: 'eq',
value: filters.category,
});
}
if (filters.minPrice !== undefined) {
list.catalog.query.addCondition({
attribute: 'price',
operator: 'gte',
value: filters.minPrice,
});
}
if (filters.maxPrice !== undefined) {
list.catalog.query.addCondition({
attribute: 'price',
operator: 'lte',
value: filters.maxPrice,
});
}
const items = list.catalog.query.items();
return { items };
} catch (error) {
console.error('Query failed:', error);
return { items: [], error: 'Search failed. Please try again.' };
}
}Handle Empty Results
Empty results are not errors, but handle them in your UI:
function searchProducts(list: DcuplList, searchTerm: string) {
list.catalog.query.clear();
if (searchTerm.trim()) {
list.catalog.query.addCondition({
attribute: 'name',
operator: 'find',
value: searchTerm,
});
}
const results = list.catalog.query.items();
const count = list.catalog.query.count();
return {
items: results,
count,
isEmpty: count === 0,
hasSearchTerm: searchTerm.trim().length > 0,
};
}
// Usage in UI
const search = searchProducts(productList, userInput);
if (search.isEmpty && search.hasSearchTerm) {
showMessage(`No products found matching "${userInput}"`);
} else if (search.isEmpty) {
showMessage('No products available');
} else {
renderProducts(search.items);
}Quality Validation Errors
Enable Quality Checks in Development
Use quality validation to catch data issues early:
const dcupl = new Dcupl();
// Enable quality validation
dcupl.models.set({
key: 'Product',
properties: [
{
key: 'name',
type: 'string',
quality: {
required: true,
nullable: false,
},
},
{
key: 'price',
type: 'float',
quality: {
required: true,
validators: {
min: { value: 0 },
},
},
},
{
key: 'email',
type: 'string',
quality: {
validators: {
email: {},
},
},
},
],
});Check for Quality Errors
After loading data, check for validation errors:
await dcupl.init();
const qualityErrors = dcupl.quality.values;
if (qualityErrors && Object.keys(qualityErrors).length > 0) {
console.warn('Data quality issues detected:', qualityErrors);
// In development: Surface errors prominently
if (process.env.NODE_ENV === 'development') {
console.table(qualityErrors);
}
// In production: Log to monitoring service
if (process.env.NODE_ENV === 'production') {
logToMonitoring('data_quality_issues', { errors: qualityErrors });
}
}Reference Integrity Errors
Handle Missing References
When referenced items do not exist, queries may return incomplete data:
function getOrderWithCustomer(
dcupl: Dcupl,
orderKey: string
): { order: Order | null; customerMissing: boolean } {
const orderList = dcupl.lists.create({ modelKey: 'Order' });
const order = orderList.catalog.getItemByKey(orderKey);
orderList.destroy();
if (!order) {
return { order: null, customerMissing: false };
}
// Check if referenced customer exists
if (order.customer) {
const customerList = dcupl.lists.create({ modelKey: 'Customer' });
const customer = customerList.catalog.getItemByKey(order.customer);
customerList.destroy();
if (!customer) {
console.warn(`Order ${orderKey} references missing customer: ${order.customer}`);
return { order, customerMissing: true };
}
}
return { order, customerMissing: false };
}Validate Data Before Loading
Check referential integrity before loading data:
interface ValidationResult {
isValid: boolean;
errors: string[];
}
function validateOrderData(orders: any[], customerKeys: Set<string>): ValidationResult {
const errors: string[] = [];
for (const order of orders) {
if (order.customer && !customerKeys.has(order.customer)) {
errors.push(`Order ${order.key} references unknown customer: ${order.customer}`);
}
}
return {
isValid: errors.length === 0,
errors,
};
}
// Usage
const customers = await fetchCustomers();
const customerKeys = new Set(customers.map((c) => c.key));
const orders = await fetchOrders();
const validation = validateOrderData(orders, customerKeys);
if (!validation.isValid) {
console.warn('Referential integrity issues:', validation.errors);
// Decide: Load anyway, skip invalid, or fail
}
dcupl.data.set(customers, { model: 'Customer' });
dcupl.data.set(orders, { model: 'Order' });Error Recovery Patterns
Fallback to Cached Data
If fresh data fails to load, use cached data:
async function loadProductsWithFallback(dcupl: Dcupl): Promise<boolean> {
try {
// Try to load fresh data
const products = await fetch('/api/products').then((r) => r.json());
dcupl.data.set(products, { model: 'Product' });
// Cache for future fallback
localStorage.setItem('products_cache', JSON.stringify(products));
localStorage.setItem('products_cache_time', Date.now().toString());
return true;
} catch (error) {
console.error('Failed to load fresh data:', error);
// Try cached data
const cached = localStorage.getItem('products_cache');
if (cached) {
const cacheTime = localStorage.getItem('products_cache_time');
const cacheAge = Date.now() - Number(cacheTime);
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
if (cacheAge < maxAge) {
console.log('Using cached data');
dcupl.data.set(JSON.parse(cached), { model: 'Product' });
return true;
} else {
console.warn('Cache too old, not using');
}
}
return false;
}
}Partial Data Loading
If some data fails, load what you can:
interface LoadResult {
loaded: string[];
failed: string[];
}
async function loadAllModels(dcupl: Dcupl): Promise<LoadResult> {
const models = [
{ key: 'Product', fetch: fetchProducts },
{ key: 'Category', fetch: fetchCategories },
{ key: 'Brand', fetch: fetchBrands },
];
const result: LoadResult = { loaded: [], failed: [] };
for (const model of models) {
try {
const data = await model.fetch();
dcupl.data.set(data, { model: model.key });
result.loaded.push(model.key);
} catch (error) {
console.error(`Failed to load ${model.key}:`, error);
result.failed.push(model.key);
}
}
// Initialize with whatever loaded
if (result.loaded.length > 0) {
await dcupl.init();
}
return result;
}
// Usage
const loadResult = await loadAllModels(dcupl);
if (loadResult.failed.length > 0) {
showWarning(`Some data could not be loaded: ${loadResult.failed.join(', ')}`);
}
if (loadResult.loaded.includes('Product')) {
renderProductList();
} else {
showError('Products unavailable');
}Graceful Degradation
Provide reduced functionality when errors occur:
class ProductService {
private dcupl: Dcupl;
private isInitialized = false;
private initError: Error | null = null;
async initialize() {
try {
this.dcupl = new Dcupl();
// ... setup models and data
await this.dcupl.init();
this.isInitialized = true;
} catch (error) {
this.initError = error as Error;
console.error('ProductService initialization failed:', error);
}
}
getProducts(filters: ProductFilters): Product[] {
if (!this.isInitialized) {
console.warn('ProductService not initialized, returning empty');
return [];
}
try {
// ... perform query
return results;
} catch (error) {
console.error('Query failed:', error);
return [];
}
}
isAvailable(): boolean {
return this.isInitialized;
}
getInitError(): Error | null {
return this.initError;
}
}
// Usage in UI
const service = new ProductService();
await service.initialize();
if (!service.isAvailable()) {
const error = service.getInitError();
showError(`Product search unavailable: ${error?.message}`);
showFallbackContent();
} else {
const products = service.getProducts(filters);
renderProducts(products);
}Logging Best Practices
Structured Error Logging
Log errors with context for debugging:
interface ErrorContext {
operation: string;
model?: string;
key?: string;
filters?: object;
timestamp: string;
}
function logError(error: Error, context: Omit<ErrorContext, 'timestamp'>) {
const entry = {
...context,
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
},
};
// Development: Console
console.error('dcupl error:', entry);
// Production: Send to monitoring service
if (process.env.NODE_ENV === 'production') {
sendToMonitoring(entry);
}
}
// Usage
try {
dcupl.data.update(updates, { model: 'Product' });
} catch (error) {
logError(error as Error, {
operation: 'updateProducts',
model: 'Product',
});
}Track Error Frequency
Monitor error patterns to identify systemic issues:
class ErrorTracker {
private errors: Map<string, number> = new Map();
private threshold = 10;
private alertSent = new Set<string>();
track(errorType: string) {
const count = (this.errors.get(errorType) || 0) + 1;
this.errors.set(errorType, count);
if (count >= this.threshold && !this.alertSent.has(errorType)) {
this.alert(errorType, count);
this.alertSent.add(errorType);
}
}
private alert(errorType: string, count: number) {
console.warn(`High error frequency: ${errorType} (${count} occurrences)`);
// Send alert to monitoring
}
getStats() {
return Object.fromEntries(this.errors);
}
reset() {
this.errors.clear();
this.alertSent.clear();
}
}
const errorTracker = new ErrorTracker();
// Usage in error handlers
try {
// ... operation
} catch (error) {
errorTracker.track('query_failed');
}Error Handling Checklist
Initialization
- Wrap
init()in try-catch - Validate model definitions
- Implement retry logic for data loading
- Provide fallback for initialization failures
Data Operations
- Validate input before updates
- Handle missing keys gracefully
- Track batch operation results
- Log failed operations with context
Queries
- Validate user-provided parameters
- Handle empty results in UI
- Return null for missing items (not throw)
- Provide meaningful error messages
Recovery
- Cache data for offline fallback
- Load partial data when possible
- Degrade gracefully on errors
- Surface errors appropriately by environment
What's Next?
- Data Modeling - Structure models effectively
- Performance - Optimize for speed
- Quality Validation - Validate data quality
- Debugging - Debug common issues