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 usefromZodErrorin a genericcatch (err)block unless you have previously checkedisZodErrorLike(err). If the error is aTypeError(e.g., calling a method on undefined),fromZodErrorwill throw an entirely new error complaining about invalid parameters. Best Practice: UsefromError()in generic catch blocks; it falls back safely. - Using
instanceofwith Monorepos: Do not useerr instanceof ZodErrororerr instanceof ValidationErrorif 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:isZodErrorLikeandisValidationErrorLike.