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
}
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
- Always use async methods - Use
.parseAsync() or .safeParseAsync() with async refinements
- Optimize async operations - Async refinements run sequentially; minimize API calls
- Handle errors gracefully - Use
.safeParseAsync() to avoid unhandled promise rejections
- Avoid mixing parse types - Don’t call
.parse() on schemas with async refinements
- Consider performance - Async validation is slower; use sync validation when possible
- 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.