Skip to main content

Basic Usage

The .catch() modifier provides a fallback value when validation fails for any reason.
import { z } from 'zod';

const schema = z.string().catch('fallback');

schema.parse('hello');     // 'hello'
schema.parse(undefined);   // 'fallback'
schema.parse(123);         // 'fallback'
schema.parse(null);        // 'fallback'
schema.parse({});          // 'fallback'
Unlike .default() which only handles undefined, .catch() handles all validation failures.

Type Inference

The .catch() modifier does not change the input/output types:
const schema = z.string().catch('fallback');

type Input = z.input<typeof schema>;   // string
type Output = z.output<typeof schema>; // string
This is because from a type perspective, you’re still working with the base type - the catch is a runtime safety mechanism.

Dynamic Fallbacks

You can provide a function to generate the fallback value and access error context:
const schema = z.string().catch((ctx) => {
  console.log('Input was:', ctx.input);
  console.log('Issues:', ctx.issues);
  return 'fallback';
});

schema.parse(123);
// Logs: Input was: 123
// Logs: Issues: [{ code: 'invalid_type', ... }]
// Returns: 'fallback'

Catch Context

The catch function receives a context object with:
  • ctx.input: The original input value that failed validation
  • ctx.issues: Array of validation issues that occurred
const schema = z.string().catch((ctx) => {
  // Convert invalid input to string as fallback
  return String(ctx.input);
});

schema.parse(123); // '123'
schema.parse(true); // 'true'

With Optional Fields

const schema1 = z.string().optional().catch('fallback');

schema1.parse(undefined); // undefined (valid for optional)
schema1.parse(123);       // 'fallback' (invalid)

// Note: undefined is valid for optional, so catch doesn't trigger

In Object Schemas

const UserSchema = z.object({
  name: z.string(),
  age: z.number().catch(0),
  role: z.enum(['user', 'admin']).catch('user'),
});

UserSchema.parse({
  name: 'John',
  age: 'invalid',  // Will use fallback: 0
  role: 'invalid', // Will use fallback: 'user'
});
// { name: 'John', age: 0, role: 'user' }
When using .catch() in objects, validation errors in fields without catch will still cause the entire parse to fail.
const schema = z.object({
  name: z.string(),           // No catch
  age: z.number().catch(0),   // Has catch
});

schema.parse({
  name: 123,      // Throws error (no catch on this field)
  age: 'invalid', // Would fallback to 0 if name was valid
});

Chaining with Transforms

const schema = z.string()
  .transform(s => s.toUpperCase())
  .catch('FALLBACK');

schema.parse('hello');  // 'HELLO'
schema.parse(123);      // 'FALLBACK'
The catch applies after the transform, so invalid inputs that can’t be transformed will use the fallback.

Nested Catches

const schema = z.object({
  inner: z.string().catch('inner-fallback'),
}).catch({
  inner: 'outer-fallback',
});

schema.parse({ inner: 123 });     // { inner: 'inner-fallback' }
schema.parse({ inner: 'valid' }); // { inner: 'valid' }
schema.parse(null);               // { inner: 'outer-fallback' }

Chained Catches

const schema = z.string()
  .catch('inner')
  .catch('outer');

schema.parse(123); // 'inner' (first catch handles it)
When catches are chained, the innermost catch handles the error first.

With Enums

enum Fruits {
  Apple = 'apple',
  Orange = 'orange',
}

const schema = z.object({
  fruit: z.nativeEnum(Fruits).catch(Fruits.Apple),
});

schema.parse({});              // { fruit: 'apple' }
schema.parse({ fruit: 15 });   // { fruit: 'apple' }
schema.parse({ fruit: 'orange' }); // { fruit: 'orange' }

Complex Example

const schema = z
  .string()
  .catch('step1')
  .transform(val => `${val}!`)
  .transform(val => val.toUpperCase())
  .catch('step2')
  .unwrap()
  .optional()
  .catch('final');

schema.parse('hello'); // 'HELLO!'
schema.parse(123);     // 'STEP1!' (caught at first catch, then transformed)

Direction-Aware Behavior

Catches only apply during parsing (forward direction), not during encoding:
const schema = z.string().catch('fallback');

// Parsing (forward)
schema.parse(123); // 'fallback'

// Encoding (reverse)
z.safeEncode(schema, 123); // returns error, no fallback

Unwrapping

You can remove the catch wrapper:
const withCatch = z.string().catch('fallback');
const withoutCatch = withCatch.unwrap(); // ZodString

// Legacy method (deprecated)
const withoutCatch2 = withCatch.removeCatch(); // same as unwrap()

Catch vs Default

Feature.catch().default()
Triggers onAny validation failureOnly undefined
Use caseError recovery, fault toleranceOptional configuration
Input typeUnchangedAdds undefined
Output typeUnchangedRemoves undefined

Use Cases

Fault-Tolerant Parsing

// Parse user-generated data with fallbacks
const UserSettings = z.object({
  theme: z.enum(['light', 'dark']).catch('light'),
  fontSize: z.number().min(10).max(24).catch(14),
  notifications: z.boolean().catch(true),
});

// Even with corrupted data, you get valid output
const settings = UserSettings.parse({
  theme: 'invalid',
  fontSize: 'not-a-number',
  notifications: 'yes',
});
// { theme: 'light', fontSize: 14, notifications: true }

API Response Sanitization

// Handle inconsistent API responses
const ApiResponse = z.object({
  items: z.array(z.object({
    id: z.string(),
    count: z.number().catch(0),
  })).catch([]),
});

// Even if API returns garbage, you get valid data
const data = ApiResponse.parse({ items: 'broken' });
// { items: [] }

Gradual Migration

// When migrating to stricter types, use catch for compatibility
const LegacyData = z.object({
  // Old API might send strings, new code expects numbers
  userId: z.number().catch(-1),
});
While .catch() is useful for fault tolerance, overuse can hide data quality issues. Use it thoughtfully and log when fallbacks are triggered.

Build docs developers (and LLMs) love