Skip to main content

Async Parsing

Zod supports asynchronous validation for schemas with async refinements or transforms.

Basic Async Parsing

Use .parseAsync() or .safeParseAsync() for async validation:
import * as z from 'zod';

const stringSchema = z.string();

const goodData = "XXX";
const goodResult = await stringSchema.safeParseAsync(goodData);

if (goodResult.success) {
  console.log(goodResult.data); // "XXX"
}

const badData = 12;
const badResult = await stringSchema.safeParseAsync(badData);

if (!badResult.success) {
  console.log(badResult.error); // ZodError
}

Sync vs Async Parse

const schema = z.string();

// Synchronous - throws immediately
try {
  schema.parse(123);
} catch (error) {
  console.log(error); // ZodError
}

// Asynchronous - returns Promise
const result = await schema.parseAsync(123).catch((error) => {
  console.log(error); // ZodError
});
If a schema contains async refinements, calling .parse() or .safeParse() will throw an error. You must use .parseAsync() or .safeParseAsync().

Async Refinements

Basic Async Refinement

Refinements can be async functions:
const schema = z.string().refine(async (_val) => {
  // Async validation logic
  return true;
});

const result = await schema.parseAsync("asdf");
console.log(result); // "asdf"

Async Refinement with Promises

const schema1 = z.string().refine((_val) => Promise.resolve(true));
const v1 = await schema1.parseAsync("asdf");
// Success: "asdf"

const schema2 = z.string().refine((_val) => Promise.resolve(false));
await schema2.parseAsync("asdf"); // Throws ZodError

Value-Based Async Validation

const schema = z.string().refine(async (val) => {
  // Access the value being validated
  return val.length > 5;
});

const r1 = await schema.safeParseAsync("asdf");
console.log(r1.success); // false

const r2 = await schema.safeParseAsync("asdf123");
console.log(r2.success); // true
console.log(r2.data); // "asdf123"

Real-World Example: Database Validation

const userSchema = z.object({
  email: z.string().email().refine(
    async (email) => {
      // Check if email already exists in database
      const existing = await db.users.findOne({ email });
      return !existing;
    },
    { message: "Email already registered" }
  ),
  username: z.string().min(3).refine(
    async (username) => {
      // Validate username availability
      const taken = await db.users.findOne({ username });
      return !taken;
    },
    { message: "Username already taken" }
  )
});

const result = await userSchema.safeParseAsync({
  email: "[email protected]",
  username: "john"
});

Async Error Behavior

Sync Parse Throws on Async Refinements

const s1 = z.string().refine(async (_val) => true);

// This throws an error!
try {
  s1.safeParse("asdf");
} catch (error) {
  // Error: Cannot use sync parse on schema with async refinements
}

Multiple Async Errors

Async validation collects all errors, just like synchronous validation:
const base = z.object({
  hello: z.string(),
  foo: z.number()
});

const testval = { hello: 3, foo: "hello" };

// Synchronous
const result1 = base.safeParse(testval);
console.log(result1.error!.issues.length); // 2

// Asynchronous - same number of errors
const result2 = await base.safeParseAsync(testval);
console.log(result2.error!.issues.length); // 2

Non-Empty String Validation

const base = z.object({
  hello: z.string().refine((x) => x && x.length > 0),
  foo: z.string().refine((x) => x && x.length > 0)
});

const testval = { hello: "", foo: "" };

const r1 = base.safeParse(testval);
const r2 = await base.safeParseAsync(testval);

// Both have the same number of issues
console.log(r1.error!.issues.length === r2.error!.issues.length); // true

Async Refinement Execution

Early Termination

Async refinements stop executing after the first failure:
let count = 0;

const schema = z.object({
  hello: z.string(),
  foo: z.number()
    .refine(async () => {
      count++;
      return true;
    })
    .refine(async () => {
      count++;
      return true;
    }, "Good")
});

const testval = { hello: "bye", foo: 3 };
const result = await schema.safeParseAsync(testval);

if (!result.success) {
  console.log(result.error.issues.length); // 1
  console.log(count); // 1 (second refinement didn't run)
}

Mixed Sync and Async Validation

const schema = z.object({
  hello: z.string(),
  foo: z.object({
    bar: z.number().refine(
      async () => new Promise((resolve) => {
        setTimeout(() => resolve(false), 500);
      })
    )
  })
});

const testval = { hello: 3, foo: { bar: 4 } };

// Sync validation
const result1 = schema.safeParse(testval); // Throws

// Async validation - collects all errors
const result2 = await schema.safeParseAsync(testval);
console.log(result2.error!.issues.length); // Multiple issues

Promise Schemas

Validate Promise values:
const promiseSchema = z.promise(z.number());

// Valid: Promise resolves to number
const goodData = Promise.resolve(123);
const goodResult = await promiseSchema.safeParseAsync(goodData);

if (goodResult.success) {
  console.log(goodResult.data); // 123 (unwrapped)
  console.log(typeof goodResult.data); // "number"
}

// Invalid: Promise resolves to wrong type
const badData = Promise.resolve("XXX");
const badResult = await promiseSchema.safeParseAsync(badData);

if (!badResult.success) {
  console.log(badResult.error); // ZodError
}

Async Transforms

While not shown in the test files, async transforms work similarly to async refinements:
const schema = z.string().transform(async (val) => {
  // Async transformation
  const result = await fetchData(val);
  return result;
});

const data = await schema.parseAsync("input");

All Schema Types Support Async

Async parsing works with all Zod schema types:
// String
await z.string().safeParseAsync("XXX");

// Number
await z.number().safeParseAsync(1234.2353);

// BigInt
await z.bigint().safeParseAsync(BigInt(145));

// Boolean
await z.boolean().safeParseAsync(true);

// Date
await z.date().safeParseAsync(new Date());

// Array
await z.array(z.string()).safeParseAsync(["XXX"]);

// Object
await z.object({ string: z.string() }).safeParseAsync({ string: "XXX" });

// Union
await z.union([z.string(), z.undefined()]).safeParseAsync(undefined);

// Record
await z.record(z.string(), z.object({})).safeParseAsync({ a: {}, b: {} });

// Literal
await z.literal("asdf").safeParseAsync("asdf");

// Enum
await z.enum(["fish", "whale"]).safeParseAsync("whale");

// Promise
await z.promise(z.number()).safeParseAsync(Promise.resolve(123));

Best Practices

  1. Always use async methods - Use .parseAsync() or .safeParseAsync() with async refinements
  2. Optimize async operations - Async refinements run sequentially; minimize API calls
  3. Handle errors gracefully - Use .safeParseAsync() to avoid unhandled promise rejections
  4. Avoid mixing parse types - Don’t call .parse() on schemas with async refinements
  5. Consider performance - Async validation is slower; use sync validation when possible
  6. Test both paths - Verify both success and failure cases in async validation
Async refinements are perfect for validating against external data sources like databases, APIs, or file systems.

Build docs developers (and LLMs) love