Skip to Content
Jumentix DocsPackages@jumentix/canaUsage guideHooks and errors

Hooks and errors

Hooks keep write-time policies close to persistence. Error guards keep recovery branches stable across storage and worker boundaries.

Write hooks

import { createClient, type CanaSchema } from '@jumentix/cana';

type Task = {
  id: string;
  title: string;
  categoryId: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  createdAt: number;
  updatedAt: number;
};

const schema: CanaSchema = {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id' },
    { name: 'tasks', keyPath: 'id', indexes: [{ name: 'byUpdatedAt', keyPath: 'updatedAt' }] }
  ]
};

const trail: string[] = [];
const client = createClient({
  name: 'tasks-hooks-demo',
  schema,
  hooks: {
    beforeWrite(context) {
      trail.push(`before:${context.store}:${context.type}`);
      if (context.store !== 'tasks' || context.type === 'deleted' || !context.record) {
        return context.record;
      }
      const task = context.record as Task;
      return { ...task, updatedAt: Date.now() };
    },
    afterCommit(events) {
      trail.push(`commit:${events.length}`);
    }
  }
});

await client.open();
await client.table('categories').put({ id: 'work', name: 'Work' });
await client.table<Task>('tasks').put({
  id: 'task-1',
  title: 'Updated by hook',
  categoryId: 'work',
  completed: false,
  priority: 'medium',
  createdAt: Date.now(),
  updatedAt: 0
});

console.log({ trail, task: await client.table<Task>('tasks').get('task-1') });

Plain-data errors

CanaError is plain data, not an Error subclass. This keeps it safe across structured clone and worker messages.

import { isCanaError, isCanaErrorCode } from '@jumentix/cana';

try {
  await client.table('categories').add({ id: 'work', name: 'Duplicate Work' });
} catch (error) {
  if (isCanaErrorCode(error, 'ConstraintViolation')) {
    console.warn('Category id already exists.');
  } else if (isCanaErrorCode(error, 'QuotaExceeded')) {
    console.warn('Storage quota is full. Ask the user to remove local data or export first.');
  } else if (isCanaError(error)) {
    console.warn(`Cana failed with ${error.code}: ${error.message}`);
  } else {
    throw error;
  }
}

Common errors

CodeTypical causeRecovery
InvalidRequestUsing a table before open() or sending an invalid key.Fix call order or key shape.
ConstraintViolationDuplicate key or unique index conflict.Show create-only conflict UI.
NotFoundupdate() target missing or replay cursor too old.Reload durable state, then subscribe again.
TransactionInactiveAwaited non-IndexedDB work inside a transaction.Move network/timer work outside the transaction body.
QuotaExceededBrowser storage quota is full.Export, clear or ask for persistent storage.
UnknownOutcomeWorker/tab died while a write was in flight.Use operationLedger and resolveWrite().

Run it here

Hooks

Run beforeWrite and afterCommit hooks around Task writes.

const trail = [];
const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
},
  hooks: {
    beforeWrite: (ctx) => { trail.push('before:' + ctx.store + ':' + ctx.type); },
    afterCommit: (events) => { trail.push('commit:' + events.length); }
  }
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
  id: 'docs-1',
  title: 'Passes through hooks',
  categoryId: 'docs',
  completed: false,
  priority: 'medium',
  createdAt: 1,
  updatedAt: 1
});
return { trail, task: await client.table('tasks').get('docs-1') };

Errors

Detect duplicate Category ids with isCanaErrorCode.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
try {
  await client.table('categories').add({ id: 'docs', name: 'Duplicada', color: '#dc2626' });
  return { unexpected: 'no error' };
} catch (error) {
  return {
    isCanaError: cana.isCanaError(error),
    constraint: cana.isCanaErrorCode(error, 'ConstraintViolation'),
    code: error && error.code
  };
}

Next

Continue to storage and crash recovery.