# Error handling

Learn how to handle errors gracefully in dcupl applications, from initialization failures to data quality issues.

## Error categories

dcupl reports problems in three ways (`summary()` is `dcupl.quality.errors.summary()`):

| Category          | Description                                       | Where you see it                                                            |
| ----------------- | ------------------------------------------------- | --------------------------------------------------------------------------- |
| **Model Errors**  | Data quality and validation issues                | `summary()` after `init()` or `update()`                                    |
| **Loader Errors** | Resources that failed to load or process          | `summary()` with `type: 'loader'`, and `failedItems` in the loader progress |
| **Thrown Errors** | Calls that can't be executed (e.g. unknown model) | An `Error` thrown by the call, see [Thrown Errors](#thrown-errors)          |

`init()` and `update()` don't throw: they resolve `false` when they fail, and the cause only goes to the console (see [When init() Resolves false](#when-init-resolves-false)).

---

## Model errors

Model errors occur when data doesn't match the expected schema or validation rules.

### Error groups

| Group                  | Description                                     |
| ---------------------- | ----------------------------------------------- |
| `ReferenceDataError`   | Reference points to missing data                |
| `PropertyDataError`    | Property value doesn't match type or validation |
| `DataContainerError`   | Issues with data container processing           |
| `ModelDefinitionError` | Invalid model configuration                     |

### Error types

Every error has an `errorType` within its group: `UndefinedValue`, `WrongDataType`, `RemoteReferenceKeyNotFound`, `MissingReference` and so on. [Quality & validation](/sdk/advanced/quality-validation#error-types) lists every model and loader error type and what triggers it.

### Handling model errors

```typescript
import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl({
  quality: { enabled: true }, // default
});

// Setup models and data...
await dcupl.init();

// Check for errors after init: one entry per (model, attribute, errorType) group
const errors = dcupl.quality.errors.summary();

if (errors.length > 0) {
  // Count errors by type
  const countsByType = errors.reduce(
    (acc, group) => {
      acc[group.errorType] = (acc[group.errorType] ?? 0) + group.totalCount;
      return acc;
    },
    {} as Record<string, number>,
  );

  console.error('Validation errors:', countsByType);

  // Handle specific error types
  if (countsByType.RemoteReferenceKeyNotFound) {
    console.warn('Some references are broken - data may be incomplete');
  }

  if (countsByType.UndefinedValue) {
    // The individual records are in the built-in $DcuplErrorTrackingErrors model
    const missing = dcupl.query.execute({
      modelKey: '$DcuplErrorTrackingErrors',
      queries: [{ attribute: 'errorType', operator: 'eq', value: 'UndefinedValue' }],
    });
    console.error(
      'Required fields missing - blocking error:',
      missing.map((e) => `${e.itemKey}.${e.attribute}`),
    );
    throw new Error('Data validation failed');
  }
}
```

### Error structure

`dcupl.quality.errors.summary()` returns one entry per group (type `QualityAnalyzer.ErrorGroupSummary` from `@dcupl/common`). `totalCount` keeps counting when `quality.maxErrorsPerGroup` (default `1000`) limits the stored records. Failed type coercions are the exception: they are consolidated into one `WrongDataType` record per attribute (affected rows in `meta.rowCount`), and when more values of one attribute fail than `maxErrorsPerGroup`, the counts are not exact (see [Limiting Stored Errors](/sdk/advanced/quality-validation#limiting-stored-errors)).

```typescript
type ErrorGroupSummary = {
  key: string; // 'Model::attribute::ErrorType'
  model?: string; // Affected model key
  attribute?: string; // Affected attribute
  errorGroup: string; // e.g. 'PropertyDataError'
  errorType: string; // e.g. 'UndefinedValue', see Error Types
  type: 'model' | 'loader';
  totalCount: number; // Number of occurrences, also beyond maxErrorsPerGroup
  storedCount: number; // Records kept in $DcuplErrorTrackingErrors
  truncated: boolean; // totalCount > storedCount
};
```

The `$DcuplErrorTrackingErrors` and `$DcuplErrorTrackingSummary` models are rebuilt by `init()` and, after a partial update, whenever an error record changed, so they and `dcupl.quality.errors.summary()` are current after `dcupl.update()`.

A partial update leaves the same error records and `summary()` counts as a fresh `init()` on the same data: the changed rows are re-validated (`unique` included), the errors of removed rows are gone, and a value of the wrong type in `data.update()` is recorded as `WrongDataType` and dropped. The item then has no value for that attribute; its previous value is not kept. One exception: a keyless row in a `data.upsert()` that a partial update applies is currently dropped without a `MissingKey` record; `init()` and a full rebuild record it.

Each record in the `$DcuplErrorTrackingErrors` model looks like this. `model`, `errorGroup` and `errorType` are references: filter them with a plain string (`value: 'UndefinedValue'`) and read them as `error.errorType.key`.

```typescript
type ErrorRecord = {
  key: string; // Generated record key
  title: string; // Human-readable title
  description?: string; // Detailed description
  itemKey?: string; // Affected item key
  attribute?: string; // Affected attribute
  meta?: Record<string, unknown>;
  groupTotal?: number; // Count of the record's group
  truncated?: boolean; // The group has more occurrences than stored records
  model?: { key: string }; // Affected model
  errorGroup: { key: string };
  errorType: { key: string };
};
```

See [Quality & validation](/sdk/advanced/quality-validation#getting-errors) for examples with real output.

---

## Loader errors

Loader errors occur when fetching or processing configuration and data resources.

### Error groups

| Group               | Description                             |
| ------------------- | --------------------------------------- |
| `LoaderConfigError` | Group of `InvalidConfig` (not reported) |
| `ResourceError`     | Failed to fetch or process a resource   |

### Error types

The SDK records `InvalidResource`, `TransformerError` and `ScriptingDisabled`; `EndpointScriptMissing` and `EndpointDuplicate` come only from REST API instances. `InvalidConfig` is never recorded: a configuration that can't be fetched or parsed makes `loader.config.fetch()` throw. [Quality & validation](/sdk/advanced/quality-validation#error-types) explains each type.

Loader errors show up in `dcupl.quality.errors.summary()` with `type: 'loader'`, also when quality checks are disabled. With quality enabled they are records in `$DcuplErrorTrackingErrors` too; the records have no `type` field, so filter them by `errorGroup` or `errorType`.

### Handling loader errors

```typescript
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

const dcupl = new Dcupl({
  config: { projectId: 'my-project', apiKey: 'my-api-key' },
});
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader); // Connect the loader before fetching its config

// Track loading errors
loader.on((progress) => {
  // Every update carries the whole run so far, so failedItems repeats earlier
  // failures. Report them once, when the run is finished.
  if (progress.remaining !== 0) return;

  for (const failure of progress.failedItems) {
    // status is the HTTP status; it is undefined for network errors (DNS, CORS, refused connection)
    const status = failure.status ? ` (HTTP ${failure.status})` : '';
    console.error(`Failed to load: ${failure.key ?? failure.url}${status}`);
  }
});

let configLoaded = false;
try {
  await loader.config.fetch(); // Throws if the loader configuration can't be fetched
  configLoaded = true;
} catch (error) {
  console.error('Failed to fetch config:', error);
  // Fallback to local config or show error UI
}

if (configLoaded) {
  // A resource that fails to load does not throw: it shows up in failedItems
  // and as an InvalidResource error with type 'loader'
  await loader.process();

  // init() doesn't throw: it resolves false when initialization failed
  const initialized = await dcupl.init();
  if (!initialized) {
    console.error('Initialization failed');
  }

  const loaderErrors = dcupl.quality.errors.summary().filter((group) => group.type === 'loader');
  if (loaderErrors.length > 0) {
    console.warn('Some resources could not be loaded:', loaderErrors);
  }
}
```

### Resource retry pattern

```typescript
// The loader must already be added with dcupl.loaders.add(loader)
async function loadWithRetry(loader: DcuplAppLoader, maxRetries = 3) {
  let lastError: Error | undefined;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await loader.config.fetch();
      await loader.process();
      return; // Success
    } catch (error) {
      lastError = error as Error;
      if (lastError.name === 'SharedModelKeyError') throw lastError; // fails the same way every time
      console.warn(`Attempt ${attempt}/${maxRetries} failed:`, error);

      if (attempt < maxRetries) {
        // Exponential backoff
        await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
      }
    }
  }

  throw lastError;
}
```

This retries when `config.fetch()` throws (the configuration couldn't be loaded) or `process()` throws (no configuration). It doesn't retry a `SharedModelKeyError` from `process()`, which fails the same way on every attempt ([Error Subclasses](#error-subclasses)). Single resources that fail to load don't throw; check `failedItems` in the progress callback or the `InvalidResource` errors in `dcupl.quality.errors.summary()`.

---

## Thrown errors

Runtime calls such as queries, lists and functions return their results directly. When a call can't be executed, it throws an `Error`; read `error.message` (three are subclasses with their own `name`, see [Error Subclasses](#error-subclasses)). These are the messages to match on:

| Message                                                                                                      | Thrown by                                                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Model with key X not found`                                                                                 | `dcupl.lists.create()` and `dcupl.query.execute()` for a model that isn't registered                                                                                                 |
| (empty message)                                                                                              | `dcupl.query.one()` and `dcupl.query.many()` for a model that isn't registered                                                                                                       |
| `invalid query - in operator needs an array value`                                                           | Any call that applies or runs an `in` condition whose value isn't an array. The list doesn't keep the condition: its query stays as it was                                           |
| `invalid query - unknown operator "X"; expected a built-in operator (…) or a registered custom operator (…)` | Any call that applies or runs a condition whose `operator` is neither built in nor registered (an [`UnknownOperator`](#error-subclasses) error). The list doesn't keep the condition |
| `There is no config present. Make sure to call loader.config.set(...) or loader.config.fetch(...) first`     | `loader.process()` before the loader has a configuration                                                                                                                             |
| `dcupl not connected. Did you add your loader to dcupl? -> dcupl.loaders.add(loader)`                        | `loader.config.fetch()` before `dcupl.loaders.add(loader)`                                                                                                                           |
| `Invalid Loader Configuration`                                                                               | `loader.config.fetch()` when the loader configuration can't be fetched or isn't JSON                                                                                                 |
| `The applicationKey X does not exist`                                                                        | `loader.process()` and `loader.resources.plan()` with an `applicationKey` the configuration doesn't define ([DcuplAppLoader](/sdk/api/loader))                                       |

Query conditions are checked by `dcupl.query.execute()`, `dcupl.lists.create({ query })`, `dcupl.fn.*` with a `query`, a list's `addCondition()`, `setCondition()`, `apply()`, `addGroup()` and `setGroup()`, and a list read that gets conditions in its options (such as `items({ queries })`). The `dcupl.data.*` calls check their payload:

| Message                                                 | Cause                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid query - missing attribute or operator`         | A condition with an `attribute` but no `operator`                                                                                                                                                                                                                                                                                                                                     |
| `invalid query - find operator cannot have array value` | A `find` condition whose value is an array                                                                                                                                                                                                                                                                                                                                            |
| `invalid query - missing query or attribute`            | A condition without `attribute` inside a group (`apply()`, `addGroup()` or `setGroup()` with a group, `lists.create({ query })`, `dcupl.query.execute()`, `dcupl.fn.*` with a `query`, or a list read with `queries` in its options such as `items({ queries })`). Passed on its own to `addCondition()`, `setCondition()` or `apply()`, such a condition is skipped without an error |
| `Invalid Data - Data must be an Array`                  | `dcupl.data.set()`, `update()`, `upsert()` and `remove()` with a payload that isn't an array, such as a single item                                                                                                                                                                                                                                                                   |
| `Invalid Data Container`                                | The same calls without a payload (`null` or `undefined`) or with options that have no `model`                                                                                                                                                                                                                                                                                         |

```typescript
try {
  dcupl.lists.create({ modelKey: 'Nope' });
} catch (error) {
  console.error((error as Error).message);
  // Model with key Nope not found
}
```

### Error subclasses

Three errors are subclasses of `Error` with their own `name` (a fourth, `DcuplSealedError`, is thrown by writes on a sealed instance, see [Performance](/sdk/advanced/performance)):

- **`ScriptNotFound`** (`Script X not found`): `dcupl.scripts.execute()` throws it, and `executeAsync()` and `executeEndpoint()` reject with it, for a script key that isn't registered. Check `error.name === 'ScriptNotFound'`, not `instanceof`: `@dcupl/core` bundles its own copy of `@dcupl/common`, so the `ScriptNotFoundError` class you import is not the class core throws ([ScriptNotFoundError](/sdk/api/types#scriptnotfounderror)). With scripting disabled, the same calls throw (or reject with) a plain `Error`, `Scripting is disabled for this dcupl instance`.
- **`UnknownOperator`** (`invalid query - unknown operator "X"; expected a built-in operator (…) or a registered custom operator (…)`): every call that checks query conditions (see above) throws it for an `operator` that is neither [built in](/sdk/api/query-operators) nor registered as an [operator script](/sdk/guides/scripts) or with `registerCustomOperator()`. The list doesn't keep the condition. Check `error.name === 'UnknownOperator'`, not `instanceof`, for the same reason as `ScriptNotFound`.
- **`SharedModelKeyError`** (`Model "X" is already loaded by app loader "a"; app loader "b" cannot load model files or data for it. …`, or `View "X" …` for a view file): `loader.process()` rejects with it when another app loader of the same dcupl instance already loads that key, and the run changes nothing. It is exported from `@dcupl/loader`, so `error instanceof SharedModelKeyError` works, and it carries `modelKey`, `owner` and `loader`. Retrying doesn't help: give each model key to one loader ([process()](/sdk/api/loader#process)).

### Handling thrown errors

```typescript
import type { DcuplList } from '@dcupl/core';
import type { DcuplListQueryOptions } from '@dcupl/common';

// dcupl methods return results directly, but you can wrap in try-catch
function safeQuery<T>(list: DcuplList, options: DcuplListQueryOptions<T>): T[] | null {
  try {
    return list.catalog.query.items(options) as T[];
  } catch (error) {
    console.error('Query failed:', error);
    return null;
  }
}

// Usage
const products = safeQuery(productList, { start: 0, count: 10 });
if (products === null) {
  // Show error state
}
```

---

## When init() resolves false

`init()` and `update()` catch their own errors. They resolve `false` and log the cause to the console as `[Dcupl] Initialization failed` or `[Dcupl] Update failed`, followed by the error. No API returns that error, and the core SDK's logger can't be redirected (see [Logging Configuration](#logging-configuration)), so in production the cause is only in the browser console. To get it, capture console errors, for example with Sentry's `captureConsoleIntegration`.

```typescript
if (!(await dcupl.init())) {
  // The cause is in the console line '[Dcupl] Initialization failed'
  throw new Error('dcupl.init() failed, see the console for the cause');
}
```

Most problems with model definitions don't fail `init()`. A reference to a model that isn't registered (`MissingModel`) or an invalid definition such as an unknown property type (`InvalidModelDefinition`) resolve `true` and show up in `summary()` in the `ModelDefinitionError` group. Check that group during development:

```typescript
const definitionErrors = dcupl.quality.errors.summary().filter((group) => group.errorGroup === 'ModelDefinitionError');

if (definitionErrors.length > 0) {
  throw new Error(`Invalid models: ${definitionErrors.map((group) => group.key).join(', ')}`);
}
```

### When data calls throw

`dcupl.data.*` calls only stage changes. They throw only for a payload that isn't an array or options without `model` ([Thrown Errors](#thrown-errors)), and problems with the data don't throw later either:

- `data.update()` ignores keys that don't exist, also on a model that has no data yet (use `data.upsert()` to add items). `data.remove()` ignores unknown keys, also on a model without data.
- Values that don't fit the model become [model errors](#model-errors), not exceptions.
- Data for a model key that isn't registered loads into no model (the rows stay staged under that key until such a model is registered). `init()` and a full rebuild log only a console line ([Data for an Unknown Model](/sdk/guides/common-mistakes#data-for-an-unknown-model)); a `data.upsert()`, `update()` or `remove()` applied by a partial update after `init()` logs nothing.
- `dcupl.query.one()` returns `null` for an item key that doesn't exist.

So validate input before you stage it, and check what `await dcupl.update()` resolves to.

---

## Event-based error handling

dcupl emits no error events: a failed `init()` or `update()` resolves `false`, and data and loader problems are collected in `dcupl.quality.errors.summary()`. Events help you spot slow operations and follow what happened before a failure. `dcupl.on()` receives the instance's own events (models, data, `init()`/`update()`, list creation); list events such as `query_execute` only reach `list.on()`.

```typescript
const dcupl = new Dcupl({
  quality: { enabled: true },
});

// Global event listener
dcupl.on((message) => {
  // Track slow operations
  if (message.duration && message.duration > 100) {
    console.warn(`Slow operation: ${message.type} took ${message.duration}ms`);
  }

  // Track staged data changes for debugging (the model is in the staged container, args[0])
  if (message.type === 'data_change') {
    const { model, type } = message.args[0];
    console.log(`Data staged (${type}):`, model);
  }
});

// List-specific error tracking
const list = dcupl.lists.create({ modelKey: 'Product' });

list.on((message) => {
  if (message.type === 'query_execute') {
    // Track query execution
    console.log(`Query executed in ${message.duration}ms`);
  }
});
```

---

## Logging configuration

In SDK 2.0, the core SDK's logging can't be configured. `@dcupl/core` writes its errors and warnings (for example `[Catalog] Invalid pivot options`) to the console at a fixed level. Neither `logger.configure()` from `@dcupl/common` nor the `logging` option of `new Dcupl({ ... })` changes that.

> **Watch out:** `logger.configure()`, `Logger.setGlobalConfig()` and `Logger.setModuleLevel()` from `@dcupl/common` don't reach the core SDK's logs (prefixes such as `Dcupl`, `Catalog`, `ModelParser`, `QualityController`). The `logging` option of `new Dcupl({ ... })` is typed but not wired; its only effect is that `level: 'debug'` prints a `[Filter] Caches invalidated` line whenever a list's caches are dropped.

What you can configure is the loader: `@dcupl/loader` logs through the shared `logger` from `@dcupl/common`. Configure it **before** you create the loader, because each loader copies the settings when it's created:

```typescript
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { logger, Logger } from '@dcupl/common';

declare const errorTracker: { captureError(entry: unknown): void }; // your error tracking service

logger.configure({
  // Log levels: 'silent' | 'error' | 'warn' | 'info' | 'debug'
  level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',

  // Output format
  format: process.env.NODE_ENV === 'development' ? 'pretty' : 'json',

  // Custom log handler for external services (replaces the console output)
  onLog: (entry) => {
    if (entry.level === 'error') {
      // e.g. '[AppLoader] Failed to load data resource' with { url, model } in entry.context
      errorTracker.captureError(entry);
    }
  },
});

// Module-specific levels by log prefix ('AppLoader', 'CsvParser', 'DcuplCsvParser'); apply immediately
Logger.setModuleLevel('CsvParser', 'error');

const dcupl = new Dcupl();
const loader = new DcuplAppLoader(); // created after configure(), so it uses these settings
dcupl.loaders.add(loader);
```

A log entry has `level`, `message`, `timestamp`, and optionally `prefix` (the module), `context` and `error`.

For the core SDK, rely on what it returns instead of its logs: `init()` and `update()` resolve `false` when they fail, failed calls throw (wrap your own calls in `try`/`catch`), and data and loader problems are listed by `dcupl.quality.errors.summary()`. The cause of a failed `init()` or `update()` is only in the console ([When init() Resolves false](#when-init-resolves-false)).

---

## Error recovery strategies

### Graceful degradation

```typescript
async function initializeDcupl() {
  const dcupl = new Dcupl({
    config: {
      projectId: process.env.DCUPL_PROJECT_ID!,
      apiKey: process.env.DCUPL_API_KEY!,
    },
  });
  const loader = new DcuplAppLoader();
  dcupl.loaders.add(loader);

  try {
    // Try to load from remote
    await loader.config.fetch();
  } catch (error) {
    console.warn('Remote config failed, using local fallback');

    // Fallback to local configuration
    loader.config.set({
      resources: [
        { type: 'model', url: '/models/Product.dcupl.json' },
        { key: 'products', type: 'data', url: '/data/products.json', model: 'Product' },
      ],
    });
  }

  await loader.process();
  if (!(await dcupl.init())) {
    throw new Error('dcupl.init() failed'); // init() resolves false instead of throwing
  }

  // Check quality even after successful init
  const errors = dcupl.quality.errors.summary();
  if (errors.length > 0) {
    const total = errors.reduce((sum, group) => sum + group.totalCount, 0);
    console.warn(`Initialized with ${total} data quality issues`);
  }

  return dcupl;
}
```

### Partial data loading

Load the critical resources first and the optional ones afterwards. A tag filter skips untagged resources, so tag every resource, including the model resources.

Data resources default to `updateType: 'set'`, which replaces all rows of their model. When an optional resource feeds the same model as a critical one, give it `updateType: 'upsert'`; otherwise the optional run replaces the rows the critical run loaded.

```typescript
loader.config.set({
  resources: [
    { type: 'model', url: '/models/Product.dcupl.json', tags: ['critical'] },
    { key: 'top-sellers', type: 'data', url: '/data/top-sellers.json', model: 'Product', tags: ['critical'] },
    {
      key: 'long-tail',
      type: 'data',
      url: '/data/long-tail.json',
      model: 'Product',
      tags: ['optional'],
      options: { updateType: 'upsert' }, // add to the critical rows instead of replacing them
    },
  ],
});
```

```typescript
// Processes the resources with the given tags and returns the ones that failed
async function processTags(loader: DcuplAppLoader, resourceTags: string[]): Promise<string[]> {
  let failed: string[] = [];
  const unsubscribe = loader.on((progress) => {
    // Each update lists every failure of the run so far: keep the latest list
    failed = progress.failedItems.map((item) => item.key ?? item.url);
  });
  try {
    await loader.process({ resourceTags });
  } finally {
    unsubscribe();
  }
  return failed;
}

// The loader is already added (dcupl.loaders.add) and has the config above
async function loadCriticalDataFirst(dcupl: Dcupl, loader: DcuplAppLoader) {
  // Load critical resources first
  const failedCritical = await processTags(loader, ['critical']);
  if (failedCritical.length > 0) {
    // Can't continue without critical data
    throw new Error(`Critical data failed to load: ${failedCritical.join(', ')}`);
  }
  if (!(await dcupl.init())) {
    throw new Error('dcupl.init() failed');
  }

  // Load optional resources afterwards
  const failedOptional = await processTags(loader, ['optional']);
  if (!(await dcupl.update())) {
    console.warn('dcupl.update() failed: the optional resources are not applied');
  }
  if (failedOptional.length > 0) {
    console.warn(`Optional resources failed: ${failedOptional.join(', ')}`);
    // Continue without them
  }
}
```

### Validation error handling

```typescript
function handleValidationErrors(dcupl: Dcupl, options: { strict: boolean }) {
  // One entry per (model, attribute, errorType) group
  const errors = dcupl.quality.errors.summary();

  // Categorize errors by severity
  const critical = errors.filter((e) => e.errorType === 'MissingModel' || e.errorType === 'InvalidModelDefinition');

  const warnings = errors.filter(
    (e) =>
      e.errorType === 'RemoteReferenceKeyNotFound' ||
      e.errorType === 'UndefinedValue' ||
      e.errorType === 'WrongDataType',
  );

  const info = errors.filter((e) => e.errorType === 'UndefinedAttribute' || e.errorType === 'UnknownColumn');

  // Report errors
  if (critical.length > 0) {
    console.error('Critical errors - cannot proceed:', critical);
    if (options.strict) {
      throw new Error('Critical validation errors');
    }
  }

  if (warnings.length > 0) {
    console.warn('Data warnings:', warnings);
  }

  if (info.length > 0 && process.env.NODE_ENV === 'development') {
    console.info('Data info:', info);
  }

  return { critical, warnings, info };
}
```

---

## Debugging tips

### Enable debug logging

There is no debug output to switch on in SDK 2.0: the core SDK's log level can't be changed, and the loader logs only errors and one warning (a `config.fetch()` option that a variable overrides), which its default level already shows. To follow what the SDK does, log its events ([Track Event Flow](#track-event-flow)) and inspect the quality summary ([Inspect Quality Metrics](#inspect-quality-metrics)).

### Inspect quality metrics

```typescript
await dcupl.init();

// Error totals per (model, attribute, errorType) group
console.table(
  dcupl.quality.errors.summary().map(({ model, attribute, errorType, type, totalCount }) => ({
    model,
    attribute,
    errorType,
    type,
    totalCount,
  })),
);

// Columns in loaded CSV files that the model doesn't declare (reported for CSV resources only)
const unknownColumns = dcupl.query.execute({
  modelKey: '$DcuplErrorTrackingErrors',
  queries: [{ attribute: 'errorType', operator: 'eq', value: 'UnknownColumn' }],
});
console.log(
  'Unused columns:',
  unknownColumns.map((e) => `${e.model?.key}.${e.attribute}`),
);
```

### Track event flow

```typescript
// Development: log the instance's events (list events go to list.on())
if (process.env.NODE_ENV === 'development') {
  dcupl.on((msg) => {
    console.log(`[dcupl] ${msg.type}`, {
      args: msg.args, // e.g. the staged container ({ model, type, data }) for data_change
      duration: msg.duration ? `${msg.duration}ms` : undefined,
    });
  });
}
```

---

## Integration with error tracking services

### Sentry integration

```typescript
import * as Sentry from '@sentry/browser';
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { logger } from '@dcupl/common';

// The core SDK logs to the console only: captureConsoleIntegration reports those errors,
// including the cause of a failed init() ('[Dcupl] Initialization failed')
Sentry.init({ dsn: 'your-dsn', integrations: [Sentry.captureConsoleIntegration({ levels: ['error'] })] });

// Loader logs: configure before creating the loader. onLog replaces the console output.
logger.configure({
  onLog: (entry) => {
    if (entry.level === 'error') {
      Sentry.captureException(entry.error ?? new Error(entry.message), {
        tags: { module: entry.prefix },
        extra: entry.context,
      });
    }
  },
});

const dcupl = new Dcupl();
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);
// ... loader.config.fetch(), loader.process()

// After init, report quality issues (including loader errors)
await dcupl.init();
const errors = dcupl.quality.errors.summary();

if (errors.length > 0) {
  Sentry.captureMessage('dcupl data quality issues', {
    level: 'warning',
    extra: { errorGroups: errors.length, errors: errors.slice(0, 10) },
  });
}
```

---

## Best practices

### 1. Always enable quality checking

```typescript
// Quality checks are on by default - keep them on, at least in development
const dcupl = new Dcupl({
  quality: { enabled: true },
});
```

### 2. Check the result and the errors after init

```typescript
if (!(await dcupl.init())) {
  throw new Error('dcupl.init() failed'); // init() resolves false instead of throwing
}
const errors = dcupl.quality.errors.summary();
// Don't ignore errors!
```

### 3. Use event listeners for real-time monitoring

```typescript
import type { DcuplUpdateMessage } from '@dcupl/common';

declare function reportSlowOperation(message: DcuplUpdateMessage): void; // yours

dcupl.on((msg) => {
  if (msg.duration && msg.duration > 100) {
    reportSlowOperation(msg);
  }
});
```

### 4. Implement graceful fallbacks

```typescript
import type { AppLoaderConfiguration } from '@dcupl/common';

declare const localFallback: AppLoaderConfiguration.ProjectConfiguration; // a configuration you ship with the app

try {
  await loader.config.fetch(); // throws when the configuration can't be loaded
} catch {
  loader.config.set(localFallback);
}
```

### 5. Log appropriately per environment

```typescript
import { logger } from '@dcupl/common';

// Applies to the loader's logs; call it before creating the loader.
// The core SDK's logging can't be configured.
logger.configure({
  level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
  format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
});
```

---

## Next steps

- [Quality & validation](/sdk/advanced/quality-validation) — Data validation rules
- [Events reference](/sdk/api/events) — All event types
- [Production Debugging](/sdk/guides/production-debugging) — Error tracking and monitoring in production
- [Debugging with Console](/sdk/guides/debugging) — Inspect your running app in dcupl Console
- [DcuplAppLoader API](/sdk/api/loader) — Loader configuration
