DeesseJS Errors

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.

basic.ts
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-email

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

access.ts
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:

runtime-validation.ts
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

valibot.ts
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: invalid

Using ArkType

arktype.ts
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: invalid

The schema overload requires a function-form message. The signature error({ fields, message: 'literal' }) is a TypeScript error: a string message is no longer accepted alongside a schema. The function receives the schema's post-transform output. The leaf's .fields slot 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:

patterns.ts
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.

See Also

On this page