Validators and Error Handling
Problem
You need to validate records before they reach storage and handle quota, disposal, scope, and migration errors predictably.
Solution
Durable Vault factories require one codec or parser schema per table. Memory may omit them. Spell schemas work directly; validatorCodec() remains available for explicit identity encoding.
Codecs
Pass any object with a parse(value): T method directly in codecs. Custom codecs validate writes through decode(encode(value)) and validate persisted values on reads.
ts
import { s } from '@vielzeug/spell';
import { table } from '@vielzeug/vault';
import { createMemory } from '@vielzeug/vault/memory';
type User = { id: number; name: string; age: number };
const schema = { users: table<User>('id') };
const db = createMemory({
schema,
codecs: {
users: s.object({
id: s.number(),
name: s.string(),
age: s.number().min(0).max(150),
}),
},
});
// rejects with a VaultError whose cause is the Spell validation error
await db.put('users', { id: 1, name: 'Alice', age: -5 });IndexedDB migration hook
ts
import { s } from '@vielzeug/spell';
import { table } from '@vielzeug/vault';
import { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';
type User = { id: number; name: string };
const schema = { users: table<User>('id', { indexes: ['name'] }) };
const UserSchema = s.object({ id: s.number(), name: s.string() });
// Object stores and schema-declared indexes are created automatically.
// The hook only handles work the schema cannot express.
const migrate: MigrationFn = ({ db, oldVersion, tx }) => {
if (oldVersion < 2 && db.objectStoreNames.contains('legacy_users')) {
tx.deleteObjectStore('legacy_users');
}
};
const db = createIndexedDB({
name: 'app',
migrate,
schema,
version: 2,
codecs: { users: UserSchema },
});
void db;Error handling
ts
import {
table,
VaultDisposedError,
VaultError,
VaultMigrationError,
VaultQuotaError,
VaultScopeError,
} from '@vielzeug/vault';
import { createMemory } from '@vielzeug/vault/memory';
type User = { id: number; name: string };
const schema = { users: table<User>('id') };
const db = createMemory({ schema });
try {
await db.put('users', { id: 1, name: 'Alice' });
} catch (err) {
if (err instanceof VaultDisposedError) {
// adapter was disposed before this call
} else if (err instanceof VaultScopeError) {
// an IndexedDB transaction accessed a table outside its declared scope
} else if (err instanceof VaultQuotaError) {
// LocalStorage / SessionStorage write exceeded quota
} else if (err instanceof VaultMigrationError) {
// IndexedDB onupgradeneeded threw
} else if (err instanceof VaultError) {
// any other vault error
} else {
throw err;
}
}Pitfalls
- Parser schemas validate writes and persisted reads. Transforming codecs validate writes with
decode(encode(value))and decode persisted values before they enter typed code. - IndexedDB codecs must preserve declared index field names and values in encoded objects.
- The
migratecallback on IndexedDB runs synchronously insideonupgradeneeded. Do not callawaitor open a second transaction inside it — IDB will throw. Errors thrown frommigratesurface asVaultMigrationErroron the first operation. - A Web Storage write that exceeds the browser quota always rejects with
VaultQuotaError; the failed write is simply not persisted.