В Zod большинство схем выполняется синхронно, поскольку проверка типов и структуры данных сводится к детерминированным операциям над уже полученным значением. Однако при появлении внешних зависимостей — запросов к API, обращения к базе данных, чтения файлов или сложных вычислений — валидация становится асинхронной.
Асинхронные сценарии реализуются через:
parseAsyncsafeParseAsyncsuperRefine с асинхронной логикойrefine в сочетании с промисамиz.promiseЛюбая асинхронная операция автоматически переносит проблему управления временем выполнения в область схемы валидации. В результате появляется необходимость учитывать таймауты и отмену операций.
Таймаут в контексте валидации — это ограничение времени, в течение которого операция должна завершиться. Если проверка длится дольше заданного порога, результат считается недействительным.
Типичные причины появления задержек:
superRefineZod не предоставляет встроенного механизма таймаутов, поэтому
управление временем выполнения реализуется на уровне обёрток над
промисами и интеграции с AbortController.
В современных JavaScript-окружениях стандартным способом отмены
асинхронных операций является AbortController.
Базовая модель:
AbortControllersignal передаётся в асинхронную операциюabort() операция должна завершиться
досрочноПример интеграции с асинхронной валидацией:
import { z } from "zod";
const schema = z.object({
userId: z.string(),
}).superRefine(async (data, ctx) => {
const controller = ctx?.signal;
const response = await fetch(`https://api.example.com/user/${data.userId}`, {
signal: controller,
});
if (!response.ok) {
ctx.addIssue({
code: "custom",
message: "Пользователь не найден",
});
}
});
Ключевая особенность — передача signal внутрь зависимых
операций. Без этого механизма отмена становится невозможной.
Одним из универсальных способов реализации ограничения времени является конкурентное выполнение промисов.
Используется конструкция Promise.race, где один из
участников — таймер:
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout exceeded")), ms)
);
return Promise.race([promise, timeout]);
}
Применение в Zod:
const schema = z.string().refine(async (value) => {
const request = fetch(`https://api.example.com/check?q=${value}`)
.then(r => r.json());
const result = await withTimeout(request, 2000);
return result.valid === true;
});
Такой подход не останавливает исходный промис, но ограничивает ожидание результата.
Таймаут и отмена часто используются как взаимозаменяемые концепции, но в контексте валидации они различаются:
Отмена предпочтительнее, поскольку:
Более корректная модель объединяет таймаут и отмену:
function withTimeoutAndAbort(promiseFactory, ms) {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, ms);
return promiseFactory(controller.signal)
.finally(() => clearTimeout(timeout));
}
Использование в Zod:
const schema = z.object({
query: z.string(),
}).superRefine(async (data, ctx) => {
await withTimeoutAndAbort(async (signal) => {
const res = await fetch(`/search?q=${data.query}`, { signal });
if (!res.ok) {
ctx.addIssue({
code: "custom",
message: "Ошибка запроса",
});
}
}, 1500);
});
В асинхронной валидации часто возникает ситуация, когда промис:
AbortSignalТакие операции приводят к блокировке цепочки parseAsync,
поскольку Zod ожидает завершения всех проверок.
Типовой источник проблемы:
await без защитыПри наличии нескольких асинхронных проверок внутри одной схемы важно учитывать конкурентное выполнение.
const schema = z.object({
email: z.string(),
}).superRefine(async (data, ctx) => {
const [exists, validDomain] = await Promise.all([
checkEmailExists(data.email),
checkDomain(data.email),
]);
if (!exists || !validDomain) {
ctx.addIssue({
code: "custom",
message: "Некорректный email",
});
}
});
Проблема возникает при необходимости отмены: Promise.all
не поддерживает частичную остановку. В таких случаях используется
AbortController внутри каждой операции.
Метод safeParseAsync возвращает результат без
выбрасывания исключений, но не изменяет поведение тайминга.
const result = await schema.safeParseAsync(data);
Если внутри схемы есть долгие операции:
Иногда таймаут применяется ко всей схеме целиком:
async function parseWithTimeout(schema, data, ms) {
return withTimeout(schema.parseAsync(data), ms);
}
Такой подход:
signal внутрьИспользуется как внешний защитный слой.
При построении схем с асинхронной логикой применяются следующие стратегии:
AbortSignal во все уровни асинхронных
вызововPromise.race только как fallbackrefineВ более новых версиях Zod появились механизмы эффектов, позволяющие разделять этапы трансформации и валидации. Это упрощает внедрение контроля выполнения, поскольку логика может быть разнесена по этапам:
Такое разделение облегчает внедрение таймаутов на уровне конкретных эффектов, а не всей схемы.
Стабильность асинхронных схем достигается через:
AbortController как стандартного
контрактаawait внутри
superRefineПри срабатывании таймаута возможны три сценария:
ctx.addIssueparseAsyncsafeParseAsyncВыбор модели зависит от архитектуры обработки ошибок, но ключевым фактором остаётся предсказуемость завершения операции.