# How to Handle and Customize Zod Parsing Errors: A Complete Guide

> Master Zod parsing errors! Learn to handle and customize ZodError messages at schema, parse-time, global, or locale levels with detailed error maps for better validation.

- Repository: [Colin McDonnell/zod](https://github.com/colinhacks/zod)
- Tags: how-to-guide
- Published: 2026-02-23

---

**Zod exposes validation failures as `ZodError` instances containing an `issues` array, and you can customize messages at four precedence levels—schema, parse-time, global, or locale—using error maps that inspect issue codes and context.**

Zod (colinhacks/zod) is a TypeScript-first schema validation library that generates detailed error reports when parsing fails. Understanding how to handle and customize Zod parsing errors lets you transform raw validation issues into user-friendly messages, structured API responses, or localized form errors without manually iterating complex nested arrays.

## Understanding the ZodError Class and Issue Structure

In [`packages/zod/src/v4/core/errors.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/errors.ts) (lines 15‑27), Zod defines the `$ZodError` class (exposed as `z.ZodError`) which stores validation failures in an `issues` array. Each issue implements `$ZodIssueBase` containing `code` (a string discriminator), `path` (location array), `message` (description), and an optional `input` property when enabled.

The full issue union type `$ZodIssue` (lines 10‑95) includes specific variants such as `$ZodIssueInvalidType` (with `expected` and `received` fields), `$ZodIssueTooBig` (with `maximum`), and `$ZodIssueTooSmall` (with `minimum`). This discriminated union structure allows error maps to generate context-aware messages by inspecting `iss.code` and related metadata.

## The Four Levels of Error Customization

According to `packages/docs/content/error-customization.mdx`, Zod applies error messages in strict precedence order. When an error map returns `undefined` or `null`, Zod falls back to the next level.

### 1. Schema-Level Messages (Highest Priority)

Override messages when defining schemas by passing a string or error map to individual validators:

```typescript
import * as z from "zod";

const schema = z.object({
  username: z.string("Username must be a string"),
  age: z
    .number()
    .int({ error: "Age must be an integer" })
    .min(0, { error: "Age cannot be negative" }),
});

```

### 2. Per-Parse Error Maps

Provide an `error` option to `parse()` or `safeParse()` for case-specific handling that overrides schema defaults but yields to global configuration:

```typescript
const result = schema.safeParse(
  { username: 123, age: -5 },
  {
    error: (iss) => {
      if (iss.code === "invalid_type") return "Wrong type supplied";
      // Returning undefined falls back to schema-level messages
    },
  }
);

```

### 3. Global Error Configuration

Call `z.config()` once to establish application-wide error handling (lower precedence than per-parse maps):

```typescript
z.config({
  customError: (iss) => {
    if (iss.code === "invalid_type")
      return `Expected ${iss.expected}, got ${typeof iss.input}`;
    // Return undefined to fall back to schema messages or locales
  },
});

```

### 4. Locale-Based Messages (Lowest Priority)

Load built-in locales for translated defaults without writing custom logic. Locales are overridden by any higher-precedence custom messages:

```typescript
import { fr } from "zod/locales";

z.config(fr()); // Default messages now render in French

```

## Transforming Errors with Utility Helpers

The same [`packages/zod/src/v4/core/errors.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/errors.ts) file exports three primary utilities (implemented across lines 62‑86, 90‑126, 138‑165, and 335‑447) to restructure error data for different consumption patterns.

### flattenError()

Converts a `ZodError` to a shallow object with `formErrors` (root-level issues) and `fieldErrors` (path-mapped arrays), ideal for form libraries like React Hook Form or Formik:

```typescript
const result = schema.safeParse({ username: 123, age: -5 });

if (!result.success) {
  const flat = z.flattenError(result.error);
  console.log(flat.fieldErrors.username); // ["Username must be a string"]
  console.log(flat.fieldErrors.age); // ["Age must be an integer", "Age cannot be negative"]
}

```

### treeifyError()

Builds a nested object mirroring your schema shape, separating top-level `errors` from nested `properties` (for objects) or `items` (for arrays). This structure suits recursive UI components that need to display errors alongside specific form fields:

```typescript
if (!result.success) {
  const errTree = z.treeifyError(result.error);
  // errTree.properties?.username?.errors => ["Username must be a string"]
  // errTree.properties?.age?.errors => ["Age must be an integer", ...]
}

```

### prettifyError()

Generates a human-readable multiline string with dot-path locations, useful for logging, CLI output, or debugging:

```typescript
console.log(z.prettifyError(result.error));
/*
✖ Username must be a string
✖ Age cannot be negative
  → at age
*/

```

## Including Raw Input in Error Reports

By default, Zod strips raw input values from the `issues` payload for privacy. Set `reportInput: true` in parse options (documented in `packages/docs/content/error-customization.mdx`, lines 40‑57) to expose the actual value that failed validation:

```typescript
const result = schema.safeParse(
  { username: 123 }, 
  { reportInput: true }
);

if (!result.success) {
  console.log(result.error.issues[0].input); // 123
}

```

## Complete Implementation Examples

### Basic Error Handling with safeParse

```typescript
import * as z from "zod";

const userSchema = z.object({
  email: z.string().email(),
  age: z.number().int().min(18),
});

const result = userSchema.safeParse({ email: "invalid", age: 16 });

if (!result.success) {
  // Access the raw issues array
  console.log(result.error.issues);
  
  // Transform for UI
  const tree = z.treeifyError(result.error);
  const flat = z.flattenError(result.error);
  const pretty = z.prettifyError(result.error);
}

```

### Context-Aware Global Error Map

```typescript
z.config({
  customError: (iss) => {
    switch (iss.code) {
      case "too_small":
        return `Value must be at least ${iss.minimum}`;
      case "too_big":
        return `Value must be at most ${iss.maximum}`;
      case "invalid_format":
        return `Invalid format: expected ${iss.format}`;
      default:
        return undefined; // Fall back to schema messages
    }
  },
});

```

### Combining Locales with Custom Overrides

```typescript
import { de } from "zod/locales";

// Set German as baseline
z.config(de());

// Override specific fields at schema level
const schema = z.object({
  password: z.string({ error: "Passwort erforderlich" }).min(8, { error: "Mindestens 8 Zeichen" }),
});

```

## Summary

- **ZodError structure**: Validation failures are instances of `z.ZodError` containing an `issues` array where each issue has `code`, `path`, `message`, and optional `input` properties defined in [`packages/zod/src/v4/core/errors.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/errors.ts).
- **Customization precedence**: Schema-level messages override per-parse maps, which override global `z.config()`, which override locale files.
- **Error maps**: Functions receive a discriminated union issue object (`$ZodIssue`) and return a string or `undefined` to delegate to the next level.
- **Formatting utilities**: Use `z.flattenError()` for form libraries, `z.treeifyError()` for nested UI components, and `z.prettifyError()` for readable logging.
- **Privacy controls**: Raw input values appear in issues only when explicitly enabling `reportInput: true` during parsing.

## Frequently Asked Questions

### How do I access the specific error code in a ZodError?

Each issue in the `issues` array contains a `code` property that acts as a discriminator. You can inspect this in error maps or when iterating over `result.error.issues`. Common codes include `"invalid_type"`, `"too_small"`, `"too_big"`, and `"invalid_format"`, each with additional context like `minimum`, `maximum`, or `expected` fields.

### Can I customize error messages based on the path?

Yes. In any error map (schema, parse, or global), inspect the `path` array on the issue object. For example, `if (iss.path[0] === "password") return "Custom password message"` lets you target specific fields while using generic logic for others.

### What is the difference between flattenError and treeifyError?

`z.flattenError()` produces a shallow structure with `formErrors` for root issues and `fieldErrors` mapped by path strings, optimized for flat form field associations. `z.treeifyError()` creates a deeply nested object that mirrors your schema structure using `properties` for object keys and `items` for array indices, better suited for recursive component trees.

### How do I show the actual value that caused the validation error?

Pass `{ reportInput: true }` as the second argument to `parse()` or `safeParse()`. This adds an `input` property to each issue object containing the raw value that failed validation. By default, Zom omits this field to prevent accidental logging of sensitive data like passwords or tokens.