Usage Guide

This guide covers common, real-world architectural patterns for utilizing zod-validation-error in your applications.

1. Differentiating Errors in API Responses (Status Codes)

A standard pattern in REST APIs (Express, Fastify, Koa, etc.) is handling validation errors differently than server errors. A validation error should yield a 400 Bad Request, whereas an unexpected code failure should yield a 500 Internal Server Error.

You should use the isValidationErrorLike type guard alongside fromError to achieve this cleanly in your error-handling middleware.

Express.js Middleware Example

import express, { Request, Response, NextFunction } from 'express';
import { z as zod } from 'zod';
import { fromError, isValidationErrorLike } from 'zod-validation-error';

const app = express();
app.use(express.json());

const payloadSchema = zod.object({
  title: zod.string().min(3),
});

app.post('/api/posts', (req, res, next) => {
  try {
    const validatedData = payloadSchema.parse(req.body);
    res.json({ success: true, data: validatedData });
  } catch (err) {
    // We caught something. Pass it to next() to hit the error handler.
    // Convert it to a ValidationError first.
    next(fromError(err));
  }
});

// Global Error Handler
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  // Is it a validation error from Zod?
  if (isValidationErrorLike(err)) {
    return res.status(400).json({
      error: 'Bad Request',
      message: err.message, // The friendly string: "Validation error: ..."
      details: err.details  // The raw Zod issues array for frontend highlighting
    });
  }

  // Otherwise, it's a generic server error
  console.error("Unhandled server error:", err);
  return res.status(500).json({
    error: 'Internal Server Error'
  });
});

2. Using ValidationError Outside of Zod

Sometimes your application contains custom, imperative validation logic that falls outside the boundaries of a Zod schema. You likely still want to throw errors that behave identically to Zod validation errors to maintain consistency in your error handling.

You can manually instantiate and throw a ValidationError.

Throwing Custom Messages

import { ValidationError } from 'zod-validation-error';
import { Buffer } from 'node:buffer';

function processFileBuffer(buf: unknown) {
  if (!Buffer.isBuffer(buf)) {
    // Throw a consistent validation error
    throw new ValidationError('Invalid argument; expected a binary buffer');
  }
  // process buffer...
}

Wrapping Other Errors (Using cause)

If a generic library throws an error during a process that you consider a "validation" phase, you can wrap it, preserving the original stack trace via the cause option.

import { ValidationError } from 'zod-validation-error';

try {
  dangerousThirdPartyParser(req.body.data);
} catch (err) {
  throw new ValidationError('Payload parsing failed during validation', { cause: err });
}

3. Integration with React Hook Form

If you are building frontends with react-hook-form and @hookform/resolvers/zod, you can leverage zod-validation-error to easily extract clean error messages for your UI.

Instead of globally registering the map, you can pass it directly to the resolver.

(Note: Depending on whether you are using Zod v3 or v4, the configuration injection differs slightly. Below assumes a general resolver structure).

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createErrorMap } from 'zod-validation-error';
import { myFormSchema } from './schemas';

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(myFormSchema, {
      errorMap: createErrorMap({ 
        // Customize how the UI sees the errors
        includePath: false, 
        forceTitleCase: true 
      })
    }),
  });

  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email')} />
      {/* This will now display beautifully formatted messages */}
      {errors.email && <p className="error">{errors.email.message}</p>}

      <button type="submit">Submit</button>
    </form>
  );
}

4. Promises and Asynchronous Code

When using asynchronous Zod schemas (.parseAsync()), you can use the .catch() block to gracefully map errors.

import { toValidationError } from 'zod-validation-error';

async function validateUser(data: unknown) {
  // toValidationError() is a curried function perfect for Promise.catch
  return await asyncUserSchema
    .parseAsync(data)
    .catch(toValidationError({ prefix: 'User Validation Failed' }));
}

Common Pitfalls

  • Blindly using fromZodError: Do not use fromZodError in a generic catch (err) block unless you have previously checked isZodErrorLike(err). If the error is a TypeError (e.g., calling a method on undefined), fromZodError will throw an entirely new error complaining about invalid parameters. Best Practice: Use fromError() in generic catch blocks; it falls back safely.
  • Using instanceof with Monorepos: Do not use err instanceof ZodError or err instanceof ValidationError if your project has complex dependencies, workspaces (monorepos), or micro-frontends. Npm/Yarn can occasionally install duplicate versions of the packages, causing prototype chains to break. Best Practice: Always use the heuristic type guards: isZodErrorLike and isValidationErrorLike.