← Назад к вопросам

Какие знаешь способы работы с асинхронным кодом?

2.2 Middle🔥 172 комментариев
#Node.js и JavaScript

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы работы с асинхронным кодом в JavaScript/Node.js

Асинхронное программирование — основа Node.js. Я использовал разные подходы в зависимости от задачи и эпохи развития проекта.

1. Callbacks (устаревший способ)

Самый ранний подход — передача функции как аргумента:

// Старомодный callback hell
fs.readFile('file.txt', (err, data) => {
  if (err) throw err;
  
  db.query('SELECT * FROM users', (err, results) => {
    if (err) throw err;
    
    const processed = results.map(r => r.name);
    fs.writeFile('output.txt', processed, (err) => {
      if (err) throw err;
      console.log('Done');
    });
  });
});

// Проблемы:
// - Callback hell (пирамида doom)
// - Сложно обработать ошибки
// - Трудно читать и поддерживать код

Использовал в legacy коде, но больше не рекомендую.

2. Promises (современный подход)

Promises — объект, представляющий будущее значение:

// Создание Promise
const getUser = (id) => {
  return new Promise((resolve, reject) => {
    db.query(`SELECT * FROM users WHERE id = $1`, [id], (err, result) => {
      if (err) reject(err);
      else resolve(result[0]);
    });
  });
};

// Использование
getUser(1)
  .then(user => {
    console.log('User:', user);
    return fs.promises.writeFile('user.json', JSON.stringify(user));
  })
  .then(() => console.log('File saved'))
  .catch(err => console.error('Error:', err))
  .finally(() => console.log('Done'));

// Promise.all — параллельные операции
Promise.all([
  getUser(1),
  getUser(2),
  getUser(3)
])
  .then(users => console.log(users))
  .catch(err => console.error(err));

// Promise.race — первое завершённое
Promise.race([
  getUser(1),
  timeout(5000)  // Timeout race condition
])
  .then(result => console.log(result))
  .catch(err => console.error('Timeout or error'));

// Promise.allSettled — все результаты (даже ошибки)
Promise.allSettled([
  getUser(1),
  getUser(999),  // Может ошибиться
  getUser(2)
])
  .then(results => {
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log(i, result.value);
      } else {
        console.log(i, 'Error:', result.reason);
      }
    });
  });

Отлично подходит для большинства случаев. Все современные API на Promises.

3. async/await (лучший способ)

Синтаксический сахар над Promises для более читаемого кода:

// async функция всегда возвращает Promise
async function main() {
  try {
    // await паузит выполнение до разрешения Promise
    const user = await getUser(1);
    console.log('User:', user);
    
    // Последовательные операции
    const file = await fs.promises.writeFile('user.json', JSON.stringify(user));
    console.log('File saved');
    
  } catch (err) {
    console.error('Error:', err);
  } finally {
    console.log('Done');
  }
}

main();

// Параллельные операции с async/await
async function getMultipleUsers() {
  try {
    // Неправильно — последовательно
    const user1 = await getUser(1);
    const user2 = await getUser(2);
    const user3 = await getUser(3);
    // Занимает: 3x времени
    
    // Правильно — параллельно
    const [user1, user2, user3] = await Promise.all([
      getUser(1),
      getUser(2),
      getUser(3)
    ]);
    // Занимает: 1x времени
    
  } catch (err) {
    console.error(err);
  }
}

// Обработка ошибок
async function robustOperation() {
  try {
    const result = await risky();
    return result;
  } catch (err) {
    if (err.code === 'ENOENT') {
      // Файл не найден
      return null;
    }
    throw err;  // Пробросить дальше
  }
}

Это мой любимый способ — читаемый, простой в отладке.

4. Event Emitters

Для работы с событиями (особенно в потоках):

const EventEmitter = require('events');

class DataProcessor extends EventEmitter {
  async process(data) {
    this.emit('start', { timestamp: Date.now() });
    
    try {
      const result = await doSomething(data);
      this.emit('success', result);
    } catch (err) {
      this.emit('error', err);
    }
  }
}

// Использование
const processor = new DataProcessor();

processor.on('start', (info) => {
  console.log('Processing started:', info);
});

processor.on('success', (result) => {
  console.log('Success:', result);
});

processor.on('error', (err) => {
  console.error('Error:', err);
});

await processor.process({ id: 1 });

Использовал для стриминга и long-running операций.

5. Streams (для больших данных)

Streams обрабатывают данные по кускам:

const fs = require('fs');

// Чтение большого файла потоком (экономит память)
fs.createReadStream('large-file.txt')
  .pipe(transformation)  // Трансформирование
  .pipe(fs.createWriteStream('output.txt'))
  .on('finish', () => console.log('Done'))
  .on('error', (err) => console.error(err));

// Или через pipeline (более безопасный способ)
const { pipeline } = require('stream');

pipeline(
  fs.createReadStream('input.txt'),
  transformation,
  fs.createWriteStream('output.txt'),
  (err) => {
    if (err) console.error('Pipeline error:', err);
    else console.log('Pipeline done');
  }
);

// Promise-based pipeline (Node 15+)
const { pipeline } = require('stream/promises');

await pipeline(
  fs.createReadStream('input.txt'),
  transformation,
  fs.createWriteStream('output.txt')
);

Критично для работы с большими файлами, видео стриминга.

6. Worker Threads

Для CPU-intensive операций (не блокирующие event loop):

const { Worker } = require('worker_threads');

// worker.js
if (require('worker_threads').isMainThread) {
  // Главный поток
  const worker = new Worker(__filename);
  
  worker.on('message', (result) => {
    console.log('Result:', result);
  });
  
  worker.postMessage({ n: 10 });
  
} else {
  // Worker поток
  const { parentPort } = require('worker_threads');
  
  parentPort.on('message', (msg) => {
    const result = fibonacci(msg.n);
    parentPort.postMessage(result);
  });
}

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Использовал для обработки изображений, шифрования.

7. Generators и async generators

Более редкий подход, но полезный:

// Generator функция
function* generateNumbers() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = generateNumbers();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

// Async generator
async function* asyncGenerator() {
  yield await Promise.resolve(1);
  yield await Promise.resolve(2);
  yield await Promise.resolve(3);
}

// Использование
for await (const value of asyncGenerator()) {
  console.log(value);
}

8. Библиотеки для асинхрона

RxJS — Reactive Extensions (сложный, но мощный):

import { interval, of } from 'rxjs';
import { map, filter, switchMap } from 'rxjs/operators';

interval(1000)
  .pipe(
    filter(x => x % 2 === 0),
    map(x => x * 2),
    switchMap(x => of(x))
  )
  .subscribe(console.log);

Bluebird — Promise утилиты (устаревает):

const Promise = require('bluebird');
const fs = require('fs');

// Promisify callback API
const readFile = Promise.promisify(fs.readFile);
const content = await readFile('file.txt');

p-queue — управление параллельными операциями:

import PQueue from 'p-queue';

const queue = new PQueue({ concurrency: 2 });

await queue.add(() => getUser(1));
await queue.add(() => getUser(2));
await queue.add(() => getUser(3));

Сравнение методов

МетодПлюсыМинусыКогда использовать
CallbacksПростой, нет overheadCallback helllegacy код
PromisesМощный, хороший контрольМногословныйlegacy API
async/awaitЧитаемый, простойНовичков путаетВСЕГДА
StreamsЭффективен для больших данныхСложный APIфайлы, video
WorkersНе блокирует event loopOverhead на созданиетяжелые вычисления
RxJSМощный для реактивностиSteep learning curvereal-time приложения

На практике

На production использую в основном async/await с Promise.all для параллелизма. Для специальных случаев:

  • Streams — обработка больших файлов
  • Worker Threads — CPU-heavy операции
  • Event Emitters — архитектура на событиях (pub/sub)
  • RxJS — real-time приложения (WebSockets, live updates)

Главное правило: всегда обрабатывай ошибки и избегай race conditions!

Какие знаешь способы работы с асинхронным кодом? | PrepBro