Опирается на правила:
NODE-20,NODE-21,NODE-X8,NODE-X9из Node Style Guide → раздел 5. Async.
Важно знать
- Только
async/await— raw.then()/.catch()-цепочки запрещены; исключение — библиотечный API, где callback обязателен по контракту.- Параллелизм —
Promise.all/Promise.allSettledпо месту, не последовательныеawait.- Каждый
Promise— awaited, returned или явно обработан: ESLint-правилаno-floating-promisesиno-misused-promisesвключены какerror.async-функция безawaitвнутри — бессмысленный шум; удалятьasyncили добавлять реальное ожидание.- Fire-and-forget (
void doStuff()) без catch-канала — unhandled rejection, которая в Node.js валит процесс.awaitне-Promise (await 42,await undefined) — шум, скрывающий реальные точки ожидания; tsc и ESLint это не поймают без type-checked пресета.no-floating-promisesиno-misused-promisesвходят вstrictTypeCheckedпресет@typescript-eslint— при правильно настроенном tooling оба правила работают без ручного включения.- Деньги в async-обработчиках —
bigint(минорные единицы); не допускатьnumberв промежуточных расчётах (NODE-18).
Асинхронный код в Node.js — это основной режим работы, а не исключение. async/await делает его читаемым: поток выполнения следует сверху вниз, try/catch покрывает ошибки так же, как в синхронном коде, а стек трейс сохраняет реальную точку возникновения ошибки. Этот раздел фиксирует, как писать async-код так, чтобы он оставался надёжным и читаемым по мере роста сервиса.
async/await вместо .then()/.catch()
NODE-20: async/await для всех промисов.
async createOrder(command: CreateOrderCommand): Promise<OrderId> {
const customer = await this.customerRepository.findById(command.customerId);
if (!customer) {
throw new CustomerNotFoundError(command.customerId);
}
const order = Order.create(customer, command.items);
await this.orderRepository.save(order);
return order.id;
}
Raw-цепочки .then()/.catch() превращают линейный поток в callback-ад:
createOrder(command: CreateOrderCommand): Promise<OrderId> {
return this.customerRepository.findById(command.customerId)
.then(customer => {
if (!customer) throw new CustomerNotFoundError(command.customerId);
return Order.create(customer, command.items);
})
.then(order => this.orderRepository.save(order).then(() => order.id));
}
Первый вариант читается как синхронный код, второй — растёт вглубь при каждом новом шаге. Стек трейс в async/await сохраняет точку await, где реально случилась ошибка; в цепочках .then() стек обрезается на промис-механизме.
Параллельные операции
Когда несколько операций независимы, Promise.all по месту:
async enrichOrder(orderId: OrderId): Promise<EnrichedOrder> {
const [order, customer, products] = await Promise.all([
this.orderRepository.findById(orderId),
this.customerRepository.findByOrderId(orderId),
this.productRepository.findByOrderId(orderId),
]);
return EnrichedOrder.from(order, customer, products);
}
Последовательные await в этом случае — трата времени:
const order = await this.orderRepository.findById(orderId);
const customer = await this.customerRepository.findByOrderId(orderId);
const products = await this.productRepository.findByOrderId(orderId);
Три независимых запроса выполняются друг за другом вместо параллельно. На реальных латентностях это заметно.
Promise.allSettled для fault-tolerant сценариев
Когда часть операций может упасть, а остальные должны выполниться:
async notifyCustomers(customerIds: readonly string[]): Promise<void> {
const results = await Promise.allSettled(
customerIds.map(id => this.notificationService.notify(id)),
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
this.logger.warn('Some notifications failed', { count: failed.length });
}
}
Promise.all прерывается при первой ошибке; Promise.allSettled ждёт всех и даёт полную картину.
Каждый Promise должен быть awaited или обработан
NODE-21: floating promise — это необработанная ошибка в ожидании.
ESLint-правила no-floating-promises и no-misused-promises из пресета strictTypeChecked делают это проверяемым:
async processPayment(orderId: OrderId): Promise<void> {
await this.paymentGateway.charge(orderId);
await this.orderRepository.markAsPaid(orderId);
}
Если нужно вернуть промис из метода — возвращаем явно:
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
return this.createOrderHandler.handle(event.toCommand());
}
Одиночный return await внутри try/catch — единственный случай, где await перед return имеет смысл: без него исключение из промиса уйдёт мимо catch:
async withRetry<T>(operation: () => Promise<T>): Promise<T> {
try {
return await operation();
} catch (error) {
this.logger.error('Operation failed, retrying', { error });
return await operation();
}
}
Запрет fire-and-forget
NODE-X9: fire-and-forget без catch-канала валит процесс.
async processOrder(command: ProcessOrderCommand): Promise<void> {
await this.orderRepository.save(command.order);
void this.auditLogger.log(command);
}
Если this.auditLogger.log завершится с ошибкой, Node.js получит unhandled rejection. В зависимости от версии и конфигурации процесс либо падает, либо глотает ошибку молча.
Если операция действительно должна быть фоновой — нужен явный catch-канал:
async processOrder(command: ProcessOrderCommand): Promise<void> {
await this.orderRepository.save(command.order);
this.auditLogger.log(command).catch(error => {
this.logger.error('Audit log failed', { error });
});
}
Лучше — вынести в отдельный метод с явным именем:
private scheduleAuditLog(command: ProcessOrderCommand): void {
this.auditLogger.log(command).catch(error => {
this.logger.error('Audit log failed', { error });
});
}
Имя scheduleAuditLog сигналит читателю: это намеренно fire-and-forget, а не пропущенный await.
async без await — бессмысленный шум
NODE-X8: async-функция без await — либо ошибка, либо лишний модификатор.
async getOrderCount(): Promise<number> {
return this.orders.length;
}
Функция не ожидает ничего асинхронного. ESLint-правило @typescript-eslint/require-await поймает это. Если функция должна соответствовать асинхронному интерфейсу — это честно указать в типе явно, без async:
getOrderCount(): Promise<number> {
return Promise.resolve(this.orders.length);
}
Или убрать Promise из возвращаемого типа, если асинхронность не нужна:
getOrderCount(): number {
return this.orders.length;
}
Симметричный антипаттерн — await не-Promise:
async createProduct(name: string): Promise<Product> {
const id = await generateId();
const label = await name.trim();
return { id, name: label };
}
name.trim() возвращает string, не Promise. await не вредит функционально, но скрывает, где реально происходит ожидание — читатель видит два await и не понимает, что ждём только одно.
NestJS: async в хендлерах и контроллерах
В NestJS весь слой обработки UseCase строится на async:
@Injectable()
export class CreateOrderHandler {
constructor(
private readonly orderRepository: OrderRepository,
private readonly customerRepository: CustomerRepository,
) {}
async handle(command: CreateOrderCommand): Promise<OrderId> {
const customer = await this.customerRepository.findById(command.customerId);
if (!customer) {
throw new CustomerNotFoundError(command.customerId);
}
const order = Order.create(customer, command.items);
await this.orderRepository.save(order);
return order.id;
}
}
Контроллер делегирует хендлеру и не содержит бизнес-логики:
@Controller('orders')
export class OrderController {
constructor(private readonly createOrderHandler: CreateOrderHandler) {}
@Post()
async createOrder(@Body() dto: CreateOrderDto): Promise<{ id: string }> {
const id = await this.createOrderHandler.handle(dto.toCommand());
return { id: id.value };
}
}
Ошибки из хендлера поднимаются до NestJS ExceptionFilter — try/catch в контроллере не нужен, если только не нужна особая трансформация ошибки в HTTP-ответ.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
.then(a => ...).catch(e => ...) вместо async/await | NODE-20 | async/await + try/catch |
Последовательные await для независимых операций | NODE-20 | Promise.all([...]) |
void this.service.doStuff() без catch-канала | NODE-X9 | .catch(error => this.logger.error(...)) |
async fn() { return value; } без await внутри | NODE-X8 | убрать async или вернуть Promise.resolve(value) |
await someString / await 42 | NODE-X8 | убрать лишний await |
Promise без await, return и без .catch() | NODE-21 | всегда await или return или .catch() |
Promise.all без обработки частичных ошибок | NODE-20 | Promise.allSettled если нужна fault-tolerance |
Куда дальше
- node/naming.md — именование async-методов: глагол,
find*vsget*,handle*для event handlers (NODE-8). - node/imports.md — named exports и path-aliases: структура, от которой зависит DI в async-хендлерах.
- node/expressions.md —
unknown+ narrowing, guard clause, явные возвращаемые типы. - node/immutability.md —
readonlyна полях,as const, spread вместо мутации аргументов. - node/tooling.md —
tsconfig.jsonstrict, ESLint strictTypeChecked, CI-прогон. - Раздел «Стандарты → Error Handling → Node» — как оформить catch-блоки, кастомные ошибки и фильтры NestJS.
- Раздел «Стандарты → Code Style» — хаб языковых биндингов: Java, Node, Python, Go.