DeesseJS Errors

Recipes

Practical examples and patterns for using @deessejs/errors in real applications.

This page collects common patterns and practical examples for using @deessejs/errors in your applications. These recipes demonstrate how to apply the library's features to solve real-world error handling problems.

Validation Errors

Validation errors are one of the most common error types. They typically include the field that failed validation and a reason for the failure. With a Standard Schema attached to the factory, the field shape is enforced at every call.

validation.ts
import { error, raise } from '@deessejs/errors';
import { z } from 'zod';

// Define validation schema
const UserSchema = z.object({
  email: z.string().email(),
  age: z.number().min(0).max(150),
});

// Create the error factory. The schema's `InferOutput` is the
// factory's `fields` type; the `message` function receives
// the post-transform output.
const ValidationError = error({
  name: 'ValidationError',
  fields: z.object({
    field: z.string(),
    reason: z.string(),
    value: z.unknown().optional(),
  }),
  message: (data) => `Validation failed for field "${data.field}": ${data.reason}`,
});

// Validation function
function validateUser(data: unknown) {
  const result = UserSchema.safeParse(data);

  if (!result.success) {
    const issue = result.error.issues[0];
    raise(
      ValidationError({
        field: issue.path.join('.'),
        reason: issue.message,
        value: (result.error as { value?: unknown }).value,
      }),
    );
  }

  return result.data;
}

When validation fails, you get an error with specific information about which field failed and why.

Network Errors

Network errors benefit from including the endpoint and any relevant request data. The schema's input is the call site shape; consumers can supply the response status without a separate field for it.

network.ts
import { error, raise } from '@deessejs/errors';
import { z } from 'zod';

const NetworkError = error({
  name: 'NetworkError',
  fields: z.object({
    endpoint: z.string(),
    method: z.string().optional(),
    statusCode: z.number().optional(),
  }),
  message: (data) => `Request to ${data.endpoint} failed`,
});

async function fetchWithError(url: string) {
  const response = await fetch(url);

  if (!response.ok) {
    raise(
      NetworkError({
        endpoint: url,
        statusCode: response.status,
      }),
    );
  }

  return response.json();
}

You can extend this pattern to include request headers, body, or timing information.

Database Errors

Database errors typically need the query that failed and any relevant identifiers. The factory's schema keeps the input shape and the runtime output type in lockstep.

database.ts
import { error } from '@deessejs/errors';
import { z } from 'zod';

const DatabaseError = error({
  name: 'DatabaseError',
  fields: z.object({
    operation: z.string(),
    table: z.string().optional(),
    query: z.string().optional(),
  }),
  message: (data) => `Database ${data.operation} failed`,
});

// Wrap database operations
async function executeQuery(db: { query: (q: string) => Promise<unknown> }, query: string, table: string) {
  try {
    return await db.query(query);
  } catch (err) {
    // `DatabaseError(...)` returns a fresh instance, then
    // `.from(err)` chains the cause. `.from()` is `this`-returning.
    // Re-throw so callers see the failure rather than a
    // resolved promise with an error value.
    throw DatabaseError({
      operation: 'query',
      table,
      query,
    }).from(err instanceof Error ? err : new Error(String(err)));
  }
}

Chaining the original error preserves the full context of what went wrong.

Error Handling Middleware

Create a reusable error handler that processes errors based on their type. Each is() check narrows the error to a specific factory's InferOutput, so err.fields.field, err.fields.endpoint, etc. are all type-checked.

handler.ts
import { error, is, causes } from '@deessejs/errors';
import { z } from 'zod';

const ValidationError = error({
  name: 'ValidationError',
  fields: z.object({ field: z.string() }),
  message: (d) => `Invalid input: ${d.field}`,
});

const NetworkError = error({
  name: 'NetworkError',
  message: () => 'External service unavailable',
});

const DatabaseError = error({
  name: 'DatabaseError',
  message: () => 'Database operation failed',
});

function handleApiError(err: unknown) {
  // Log the full error chain
  console.error('Error chain:', causes(err).map((e) => e.message));

  if (is(err, ValidationError)) {
    return {
      status: 400,
      message: `Invalid input: ${err.fields.field}`,
    };
  }

  if (is(err, NetworkError)) {
    return {
      status: 503,
      message: 'External service unavailable',
    };
  }

  if (is(err, DatabaseError)) {
    return {
      status: 500,
      message: 'Database operation failed',
    };
  }

  // Generic fallback
  return {
    status: 500,
    message: 'Internal server error',
  };
}

This pattern centralizes error handling logic and ensures consistent responses.

Service Layer Error Wrapping

When building services that call other services, wrap errors to add context while preserving the cause chain. .from() replaces the previous direct cause; build the chain by attaching each layer's cause to the previous one.

service.ts
import { error } from '@deessejs/errors';
import { z } from 'zod';

const AppError = error({ name: 'AppError' });

const UserNotFoundError = error({
  name: 'UserNotFoundError',
  fields: z.object({ userId: z.string() }),
  message: (d) => `User ${d.userId} not found`,
});

class UserService {
  async getUser(userId: string, fetchUser: (id: string) => Promise<unknown>) {
    try {
      const user = await fetchUser(userId);
      if (!user) {
        // `.raise()` is the idiomatic throw; here we use
        // throw since we're in a class method.
        throw UserNotFoundError({ userId });
      }
      return user;
    } catch (err) {
      // Wrap any error from lower layers. `err` is `unknown`
      // in strict mode; check before .from(err).
      const cause = err instanceof Error ? err : new Error(String(err));
      throw AppError().from(cause);
    }
  }
}

The caller gets both the context of the current operation and the underlying cause.

Hierarchical Error Categories

Create a hierarchy that lets you handle errors at different levels of granularity. is() walks the factory's inherits graph, so a single check can match a leaf and any of its ancestors.

hierarchy.ts
import { error, is } from '@deessejs/errors';
import { z } from 'zod';

// Top level
const AppError = error({ name: 'AppError' });

// Domain level
const UserError = error({ name: 'UserError', inherits: AppError });
const ProductError = error({ name: 'ProductError', inherits: AppError });

// Specific errors. Each leaf carries its own schema; the
// InferOutput stays scoped to that factory.
const UserNotFoundError = error({
  name: 'UserNotFoundError',
  fields: z.object({ userId: z.string() }),
  message: (d) => `User ${d.userId} not found`,
  inherits: UserError,
});

const UserPermissionError = error({
  name: 'UserPermissionError',
  fields: z.object({ userId: z.string(), action: z.string() }),
  message: (d) => `User ${d.userId} cannot ${d.action}`,
  inherits: UserError,
});

function handleError(err: unknown) {
  if (is(err, AppError)) {
    // Catches everything in the AppError family.
  }
  if (is(err, UserError)) {
    // Catches UserNotFoundError, UserPermissionError.
  }
  if (is(err, UserNotFoundError)) {
    // Catches only user not found errors; `err.fields.userId`
    // is typed as `string`.
  }
}

This lets you write both broad handlers and specific ones depending on the situation.

See Also

On this page