.md

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() or dcupl.update() fails: it logs the error and resolves false. Check the boolean it resolves to.
  • Invalid model definition: dcupl records an InvalidModelDefinition quality error, and init() still resolves true.
  • Invalid or missing values: dcupl records quality errors such as WrongDataType or UndefinedValue (attributes are required by default).
  • Unknown key in data.update() or remove(): ignored. Check that the item exists first, or use data.upsert().
  • Unknown key in query.one(): returns null. Handle null in 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 (in with a value that isn't an array, find with an array, no operator): throws invalid query - … and isn't stored, so the list's query stays as it was.
  • Unknown model key: lists.create() and query.execute() throw "Model with key X not found", and query.one() throws an Error with an empty message. Create lists after await 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 logs Model not found during data processing and still resolves true. 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:

retry-loading.ts
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:

cache-fallback.ts
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:

empty-results.ts
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() and dcupl.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 null from query.one() and empty results in the UI
  • Validate user-provided filter values before querying
  • Log failed operations with context