Таймауты и отмена операций

В Zod большинство схем выполняется синхронно, поскольку проверка типов и структуры данных сводится к детерминированным операциям над уже полученным значением. Однако при появлении внешних зависимостей — запросов к API, обращения к базе данных, чтения файлов или сложных вычислений — валидация становится асинхронной.

Асинхронные сценарии реализуются через:

  • parseAsync
  • safeParseAsync
  • superRefine с асинхронной логикой
  • refine в сочетании с промисами
  • z.promise

Любая асинхронная операция автоматически переносит проблему управления временем выполнения в область схемы валидации. В результате появляется необходимость учитывать таймауты и отмену операций.


Природа таймаутов в схемах валидации

Таймаут в контексте валидации — это ограничение времени, в течение которого операция должна завершиться. Если проверка длится дольше заданного порога, результат считается недействительным.

Типичные причины появления задержек:

  • сетевые запросы в superRefine
  • обращения к внешним сервисам
  • последовательные асинхронные проверки
  • зависшие промисы без механизма отмены
  • неоптимальные цепочки трансформаций

Zod не предоставляет встроенного механизма таймаутов, поэтому управление временем выполнения реализуется на уровне обёрток над промисами и интеграции с AbortController.


AbortController как базовый механизм отмены

В современных JavaScript-окружениях стандартным способом отмены асинхронных операций является AbortController.

Базовая модель:

  • создаётся AbortController
  • его signal передаётся в асинхронную операцию
  • при вызове 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 внутрь зависимых операций. Без этого механизма отмена становится невозможной.


Таймаут через race condition

Одним из универсальных способов реализации ограничения времени является конкурентное выполнение промисов.

Используется конструкция 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;
});

Такой подход не останавливает исходный промис, но ограничивает ожидание результата.


Различие между таймаутом и отменой

Таймаут и отмена часто используются как взаимозаменяемые концепции, но в контексте валидации они различаются:

  • Таймаут прерывает ожидание результата на уровне контроля исполнения
  • Отмена останавливает саму операцию внутри источника данных

Отмена предпочтительнее, поскольку:

  • освобождает ресурсы (сеть, CPU)
  • предотвращает утечки памяти
  • уменьшает нагрузку на серверы
  • позволяет корректно завершать цепочки зависимостей

Интеграция AbortController в withTimeout

Более корректная модель объединяет таймаут и отмену:

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 ожидает завершения всех проверок.

Типовой источник проблемы:

  • HTTP-клиенты без timeout
  • сторонние SDK без cancellation API
  • последовательные 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 и влияние времени выполнения

Метод safeParseAsync возвращает результат без выбрасывания исключений, но не изменяет поведение тайминга.

const result = await schema.safeParseAsync(data);

Если внутри схемы есть долгие операции:

  • весь вызов блокируется до завершения
  • отсутствует автоматическое прерывание
  • ошибки таймаута должны обрабатываться вручную

Ограничение времени на уровне обёртки схемы

Иногда таймаут применяется ко всей схеме целиком:

async function parseWithTimeout(schema, data, ms) {
  return withTimeout(schema.parseAsync(data), ms);
}

Такой подход:

  • не требует модификации схем
  • не передаёт signal внутрь
  • не освобождает ресурсы внутри операций

Используется как внешний защитный слой.


Стратегии устойчивой отмены

При построении схем с асинхронной логикой применяются следующие стратегии:

  • передача AbortSignal во все уровни асинхронных вызовов
  • использование Promise.race только как fallback
  • ограничение числа параллельных проверок
  • изоляция долгих операций в отдельные функции
  • избегание неотменяемых промисов внутри refine

Эффекты Zod и контроль жизненного цикла

В более новых версиях Zod появились механизмы эффектов, позволяющие разделять этапы трансформации и валидации. Это упрощает внедрение контроля выполнения, поскольку логика может быть разнесена по этапам:

  • предварительная трансформация
  • синхронная проверка
  • асинхронная проверка
  • постобработка результата

Такое разделение облегчает внедрение таймаутов на уровне конкретных эффектов, а не всей схемы.


Практика предотвращения деградации времени выполнения

Стабильность асинхронных схем достигается через:

  • обязательные timeout-параметры для всех внешних запросов
  • использование AbortController как стандартного контракта
  • минимизацию последовательных await внутри superRefine
  • отказ от неуправляемых промисов
  • централизованное управление ограничениями времени

Поведение при истечении времени

При срабатывании таймаута возможны три сценария:

  • возврат ошибки валидации через ctx.addIssue
  • выброс исключения из parseAsync
  • возврат специального результата через safeParseAsync

Выбор модели зависит от архитектуры обработки ошибок, но ключевым фактором остаётся предсказуемость завершения операции.