.md

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

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).


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 lists every model and loader error type and what triggers it.

Handling model errors

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).

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.

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 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 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

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

// 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). 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). 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). 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)

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
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):

  • 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). 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 nor registered as an operator script 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()).

Handling thrown errors

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), so in production the cause is only in the browser console. To get it, capture console errors, for example with Sentry's captureConsoleIntegration.

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:

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), 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, 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); 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().

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:

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).


Error recovery strategies

Graceful degradation

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.

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
    },
  ],
});
// 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

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) and inspect the quality summary (Inspect Quality Metrics).

Inspect quality metrics

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

// 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

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

// 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

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

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

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

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',
});