JS Tower
Node.jsМодуль 1: Введение в Node.js

Асинхронность в Node.js

Callbacks, Promises и async/await

Цель урока

В этом уроке ты научишься:

  • Работать с callbacks
  • Использовать Promises
  • Применять async/await
  • Выполнять параллельные операции

Callbacks

Традиционный подход в Node.js — Error-First Callbacks:

const fs = require('fs');

// Первый аргумент — ошибка, второй — результат
fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Ошибка:', err.message);
    return;
  }
  console.log('Данные:', data);
});

Callback Hell

// Проблема — вложенные callbacks
fs.readFile('file1.txt', 'utf8', (err, data1) => {
  if (err) return console.error(err);
  
  fs.readFile('file2.txt', 'utf8', (err, data2) => {
    if (err) return console.error(err);
    
    fs.readFile('file3.txt', 'utf8', (err, data3) => {
      if (err) return console.error(err);
      
      console.log(data1, data2, data3);
    });
  });
});

Promises

Промисификация

const fs = require('fs');
const util = require('util');

// Преобразование callback в Promise
const readFile = util.promisify(fs.readFile);

readFile('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

fs/promises

const fs = require('fs/promises');

// Встроенные промисы
fs.readFile('file.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

Цепочки

const fs = require('fs/promises');

fs.readFile('file1.txt', 'utf8')
  .then(data1 => {
    console.log('Файл 1:', data1);
    return fs.readFile('file2.txt', 'utf8');
  })
  .then(data2 => {
    console.log('Файл 2:', data2);
    return fs.readFile('file3.txt', 'utf8');
  })
  .then(data3 => {
    console.log('Файл 3:', data3);
  })
  .catch(err => console.error('Ошибка:', err));

async/await

const fs = require('fs/promises');

async function readFiles() {
  try {
    const data1 = await fs.readFile('file1.txt', 'utf8');
    console.log('Файл 1:', data1);
    
    const data2 = await fs.readFile('file2.txt', 'utf8');
    console.log('Файл 2:', data2);
    
    const data3 = await fs.readFile('file3.txt', 'utf8');
    console.log('Файл 3:', data3);
  } catch (err) {
    console.error('Ошибка:', err.message);
  }
}

readFiles();

Параллельное выполнение

Promise.all

const fs = require('fs/promises');

async function readAllFiles() {
  try {
    const [data1, data2, data3] = await Promise.all([
      fs.readFile('file1.txt', 'utf8'),
      fs.readFile('file2.txt', 'utf8'),
      fs.readFile('file3.txt', 'utf8')
    ]);
    
    console.log(data1, data2, data3);
  } catch (err) {
    console.error('Одна из операций завершилась ошибкой');
  }
}

Promise.allSettled

const results = await Promise.allSettled([
  fs.readFile('exists.txt', 'utf8'),
  fs.readFile('not-exists.txt', 'utf8'),
  fs.readFile('another.txt', 'utf8')
]);

results.forEach((result, index) => {
  if (result.status === 'fulfilled') {
    console.log(`Файл ${index}: ${result.value}`);
  } else {
    console.log(`Файл ${index}: Ошибка - ${result.reason.message}`);
  }
});

Promise.race

// Первый завершившийся
const result = await Promise.race([
  fetch('https://api1.example.com'),
  fetch('https://api2.example.com')
]);

Обработка ошибок

try/catch

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Ошибка загрузки:', error.message);
    throw error; // Пробрасываем дальше
  }
}

Глобальная обработка

// Необработанные промисы
process.on('unhandledRejection', (reason, promise) => {
  console.error('Необработанный промис:', reason);
});

// Необработанные исключения
process.on('uncaughtException', (error) => {
  console.error('Необработанное исключение:', error);
  process.exit(1);
});

Паттерны

Retry

async function fetchWithRetry(url, retries = 3, delay = 1000) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fetch(url);
    } catch (error) {
      if (i === retries - 1) throw error;
      
      console.log(`Попытка ${i + 1} не удалась, повтор через ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Timeout

function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
  
  return Promise.race([promise, timeout]);
}

// Использование
try {
  const data = await withTimeout(fetch(url), 5000);
} catch (error) {
  if (error.message === 'Timeout') {
    console.log('Превышено время ожидания');
  }
}

Throttle

async function processWithThrottle(items, fn, concurrency = 5) {
  const results = [];
  
  for (let i = 0; i < items.length; i += concurrency) {
    const batch = items.slice(i, i + concurrency);
    const batchResults = await Promise.all(batch.map(fn));
    results.push(...batchResults);
  }
  
  return results;
}

// Использование
const urls = ['url1', 'url2', /* ... много URL */];
const data = await processWithThrottle(urls, fetch, 5);

Практика

Задание 1: Последовательное чтение

Задача: Прочитай 3 файла последовательно.

Решение:

const fs = require('fs/promises');

async function readSequentially(files) {
  const results = [];
  
  for (const file of files) {
    const data = await fs.readFile(file, 'utf8');
    results.push(data);
  }
  
  return results;
}

readSequentially(['a.txt', 'b.txt', 'c.txt'])
  .then(console.log)
  .catch(console.error);

Задание 2: Параллельное чтение

Задача: Прочитай 3 файла параллельно.

Решение:

const fs = require('fs/promises');

async function readParallel(files) {
  return Promise.all(files.map(file => fs.readFile(file, 'utf8')));
}

readParallel(['a.txt', 'b.txt', 'c.txt'])
  .then(console.log)
  .catch(console.error);

Задание 3: Retry

Задача: Реализуй функцию с повторными попытками.

Решение:

async function retry(fn, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries - 1) throw error;
      console.log(`Попытка ${i + 1} не удалась`);
    }
  }
}

// Использование
await retry(() => fetch('https://api.example.com'), 3);

Проверь себя

  1. Что такое Error-First Callback?
  2. Чем Promise.all отличается от Promise.allSettled?
  3. Как обработать таймаут промиса?