Fields and Schema
Define structured fields and validation schemas for error types in @deessejs/errors.
Errors become much more useful when they carry structured data about what went wrong. @deessejs/errors lets you define fields on your error factories and optionally validate them using any Standard Schema compatible library.
Defining Fields
With a Standard Schema, the field shape is the schema's InferOutput. The factory's call signature requires the schema's InferInput. Inputs that don't validate throw ArgsValidationError at construction.
import { z } from "zod";
import { error } from "@deessejs/errors";
const ValidationError = error({
name: "ValidationError",
fields: z.object({
field: z.string(),
reason: z.string(),
value: z.unknown().optional(),
}),
message: (data) => `Field "${data.field}": ${data.reason}`,
});
const err = ValidationError({
field: "email",
reason: "invalid format",
});
console.log(err.fields.field); // -> email
const err2 = ValidationError({
field: "email",
reason: "invalid format",
value: "not-an-email",
});
console.log(err2.fields.value); // -> not-an-emailThe fields object is always accessible on the error instance, giving you a consistent place to look for error context.
Accessing Field Values
Once an error is caught, you can access its fields to provide meaningful feedback or logging:
import { z } from "zod";
import { error, is, raise } from "@deessejs/errors";
const ValidationError = error({
name: "ValidationError",
fields: z.object({ field: z.string(), reason: z.string() }),
message: (data) => `Field ${data.field}: ${data.reason}`,
});
try {
try {
raise(ValidationError({ field: "email", reason: "not a valid email address" }));
} catch (err) {
// err is `unknown` in strict mode; narrow before reading.
if (is(err, ValidationError)) {
console.log(`Error in field "${err.fields.field}": ${err.fields.reason}`);
}
}
} catch (err) {
// The outer try swallows the throw so the example
// terminates cleanly.
}The expected output is Error in field "email": not a valid email address. For full type inference in catch blocks, use the is() function with type narrowing.
Runtime Validation via Standard Schema
For production-grade error definitions, attach a Standard Schema validator to fields. Any conforming library works — Zod, Valibot, ArkType, and any future implementation.
Inputs that don't validate throw an ArgsValidationError carrying the vendor name and the failure issues:
import { z } from "zod";
import { ArgsValidationError, error } from "@deessejs/errors";
const ValidationError = error({
name: "ValidationError",
fields: z.object({
field: z.string().min(1),
reason: z.string().min(1),
}),
message: (data) => `Field "${data.field}": ${data.reason}`,
});
// Valid input -> rendered message
const ok = ValidationError({ field: "email", reason: "invalid" });
console.log(ok.message); // -> Field "email": invalid
// Invalid input -> ArgsValidationError
try {
ValidationError({ field: "", reason: "invalid" });
} catch (err) {
if (err instanceof ArgsValidationError) {
console.log(err.vendor); // -> zod
console.log(err.issues.length > 0); // -> true
}
}Using Valibot
import * as v from "valibot";
import { error } from "@deessejs/errors";
const ValidationError = error({
name: "ValidationError",
fields: v.object({
field: v.pipe(v.string(), v.minLength(1)),
reason: v.pipe(v.string(), v.minLength(1)),
}),
message: (data) => `Field ${data.field}: ${data.reason}`,
});
const err = ValidationError({ field: "email", reason: "invalid" });
console.log(err.message); // -> Field email: invalidUsing ArkType
import { type } from "@ark/type";
import { error } from "@deessejs/errors";
const ValidationError = error({
name: "ValidationError",
fields: type({
field: "string",
reason: "string",
}),
message: (data) => `Field ${data.field}: ${data.reason}`,
});
const err = ValidationError({ field: "email", reason: "invalid" });
console.log(err.message); // -> Field email: invalidThe schema overload requires a function-form
message. The signatureerror({ fields, message: 'literal' })is a TypeScript error: a stringmessageis no longer accepted alongside a schema. The function receives the schema's post-transform output. The leaf's.fieldsslot carries the same output.
Why Use Schema Validation?
Schema validation in error factories provides several benefits:
Early error detection — Configuration mistakes in your error definitions are caught immediately rather than causing subtle bugs later.
Self-documenting code — The schema serves as documentation for what data each error type expects.
Consistent data — All errors of a given type have the same structure, making logging and monitoring easier.
Vendor-neutral — Switch between Zod, Valibot, ArkType, or any future Standard Schema implementation without changing your error definitions.
Common Field Patterns
Here are some common patterns for error fields:
import { z } from "zod";
import { error } from "@deessejs/errors";
const DatabaseError = error({
name: "DatabaseError",
fields: z.object({
query: z.string(),
table: z.string().optional(),
code: z.string().optional(),
}),
message: (data) => `${data.query} failed`,
});
const ApiError = error({
name: "ApiError",
fields: z.object({
endpoint: z.string(),
statusCode: z.number(),
response: z.unknown().optional(),
}),
message: (data) => `Request to ${data.endpoint} returned ${data.statusCode}`,
});
const ValidationError = error({
name: "ValidationError",
fields: z.object({
field: z.string(),
reason: z.string(),
value: z.unknown().optional(),
constraints: z.string().optional(),
}),
message: (data) => `${data.field}: ${data.reason}`,
});Design your fields to capture what's useful for debugging and logging, not just what's required to identify the error.