Error handling best practices
Robust error handling keeps your application stable and gives users a good experience even when things go wrong. This page covers the principles and the patterns that belong in your app: retries, fallbacks and empty states. How the SDK reports errors (error groups and types, loader failures, quality errors) is described in the SDK guide Error handling.
Error handling principles
- Anticipate failures - Assume network requests and data operations can fail
- Check results - The SDK reports many problems through return values and quality errors, not exceptions
- Fail gracefully - Provide fallbacks and meaningful error messages
- Log for debugging - Capture enough context (operation, model, key) to diagnose issues
- Recover when possible - Retry transient failures automatically
Know what the SDK does instead of throwing
Most dcupl calls don't throw when something is wrong. Check for each case:
init()ordcupl.update()fails: it logs the error and resolvesfalse. Check the boolean it resolves to.- Invalid model definition: dcupl records an
InvalidModelDefinitionquality error, andinit()still resolvestrue. - Invalid or missing values: dcupl records quality errors such as
WrongDataTypeorUndefinedValue(attributes are required by default). - Unknown key in
data.update()orremove(): ignored. Check that the item exists first, or usedata.upsert(). - Unknown key in
query.one(): returnsnull. Handlenullin the UI. - Unknown attribute or operator in a condition: an empty result, with no error or log. Check the names against the model.
- Malformed condition (
inwith a value that isn't an array,findwith an array, nooperator): throwsinvalid query - …and isn't stored, so the list's query stays as it was. - Unknown model key:
lists.create()andquery.execute()throw "Model with key X not found", andquery.one()throws an Error with an empty message. Create lists afterawait dcupl.init(), and check the key. data.set()for a model that isn't registered: the rows don't load into any model.init()only logsModel not found during data processingand still resolvestrue. See Data for an Unknown Model.
Quality errors don't stop anything, so read them after loading, at least during development:
const qualityErrors = dcupl.quality.errors.summary();
if (qualityErrors.length > 0) console.warn('Data quality issues:', qualityErrors);For an empty result you didn't expect, see Query Returns No Results. For checking data quality after loading, see Quality Validation.
App-level patterns
The snippets use placeholder helpers from your app: showErrorMessage(), showMessage() and renderProducts().
Retry failed data loading
Network requests can fail. Retry with exponential backoff before you give up:
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 }); // staged until init() or update()
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.');
}When you load through DcuplAppLoader, the loader reports failed resources instead; see Resource Retry Pattern.
Fall back to cached data
If fresh data fails to load, use the last copy you cached:
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;
}
}Validate user input before querying
Check filter values from the UI (a negative price, a minimum above the maximum) before they reach the query, and show a message instead of an empty result.
Handle empty results
Empty results are not errors, but the UI should say why it is empty:
function searchProducts(list: DcuplList, searchTerm: string) {
list.catalog.query.clear();
if (searchTerm.trim()) {
// Wrap the term in slashes for substring search; a bare string must match the whole value.
// Escape regex characters in user input first.
const escaped = searchTerm.trim().replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
list.catalog.query.addCondition({
attribute: 'name',
operator: 'find',
value: `/${escaped}/`,
options: { regexFlags: 'i' },
});
}
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);
}Degrade gracefully
If init() resolves false, keep the rest of the page usable: show the error and fallback content instead of the data-driven parts. The SDK guide shows Graceful Degradation with a local fallback configuration and Partial Data Loading with critical and optional resources.
Log with context
Log the operation, the model and the key with every error, and send errors to your monitoring service in production. See Integration with Error Tracking Services.
Error handling checklist
- Check the boolean
init()anddcupl.update()resolve to (they don't throw) - Check
dcupl.quality.errors.summary()after loading, at least during development - Retry transient load failures, and fall back to cached data
- Check that an item exists before you update it
- Handle
nullfromquery.one()and empty results in the UI - Validate user-provided filter values before querying
- Log failed operations with context