Как велась асинхронная разработка в Node.js до появления async/await?

Ответ

До ES2017 и async/await асинхронный код в Node.js писали с помощью колбэков и промисов, что часто приводило к сложностям в поддержке.

1. Эра колбэков (Callback Hell / Pyramid of Doom): Основной паттерн в ранних версиях Node.js. Глубоко вложенные колбэки быстро становились нечитаемыми.

const fs = require('fs');

function processFiles(callback) {
  fs.readFile('data.json', 'utf8', (err, config) => {
    if (err) return callback(err);
    const configObj = JSON.parse(config);
    fs.readFile(configObj.inputFile, 'utf8', (err, data) => {
      if (err) return callback(err);
      const processed = data.toUpperCase();
      fs.writeFile(configObj.outputFile, processed, (err) => {
        if (err) return callback(err);
        fs.readFile('log.txt', 'utf8', (err, log) => {
          if (err) return callback(err);
          const newLog = log + 'nProcessing complete';
          fs.writeFile('log.txt', newLog, (err) => {
            callback(err, 'Done');
          });
        });
      });
    });
  });
}

2. Библиотеки для управления колбэками: Чтобы справиться с этим, широко использовалась библиотека async.js.

const async = require('async');

async.waterfall([
  (callback) => fs.readFile('data.json', 'utf8', callback),
  (config, callback) => {
    const configObj = JSON.parse(config);
    fs.readFile(configObj.inputFile, 'utf8', callback);
  },
  (data, callback) => {
    const processed = data.toUpperCase();
    fs.writeFile('output.txt', processed, (err) => callback(err, processed));
  }
], (err, result) => {
  if (err) console.error('Error:', err);
  else console.log('Result:', result);
});

3. Приход промисов (Promises): С появлением нативных промисов (ES6) код стал структурированнее, но оставались длинные цепочки .then() и .catch().

const { readFile, writeFile } = require('fs').promises; // Используем fs.promises

function processWithPromises() {
  return readFile('data.json', 'utf8')
    .then(config => {
      const configObj = JSON.parse(config);
      return readFile(configObj.inputFile, 'utf8');
    })
    .then(data => {
      const processed = data.toUpperCase();
      return writeFile('output.txt', processed).then(() => processed);
    })
    .then(result => {
      console.log('Success:', result);
      return result;
    })
    .catch(err => {
      console.error('Failed:', err);
      throw err;
    });
}

Почему async/await стал революцией? Он позволил писать асинхронный код в почти синхронном стиле, сохраняя всю мощь промисов, но радикально повышая читаемость и упрощая обработку ошибок через try/catch.

// Тот же функционал с async/await
async function processWithAsyncAwait() {
  try {
    const config = await readFile('data.json', 'utf8');
    const configObj = JSON.parse(config);
    const data = await readFile(configObj.inputFile, 'utf8');
    const processed = data.toUpperCase();
    await writeFile('output.txt', processed);
    console.log('Success:', processed);
    return processed;
  } catch (err) {
    console.error('Failed:', err);
    throw err;
  }
}

Переход на async/await значительно снизил количество ошибок, связанных с потоком выполнения, и упростил рефакторинг и отладку асинхронного кода.

Ответ 18+ 🔞

Э, представляешь, как мы раньше жили-то? До того как async/await придумали, асинхронный код в Node.js писался на колбэках и промисах. И это была такая пиздопроебибна, что волосы дыбом вставали, честное слово.

1. Эра колбэков (Callback Hell / Пирамида Судьбы): Это, блядь, основной паттерн был в раннем Node.js. Вложенные колбэки так глубоко уходили, что читать это было невозможно, ёпта. Прямо пирамида какая-то проклятая.

const fs = require('fs');

function processFiles(callback) {
  fs.readFile('data.json', 'utf8', (err, config) => {
    if (err) return callback(err);
    const configObj = JSON.parse(config);
    fs.readFile(configObj.inputFile, 'utf8', (err, data) => {
      if (err) return callback(err);
      const processed = data.toUpperCase();
      fs.writeFile(configObj.outputFile, processed, (err) => {
        if (err) return callback(err);
        fs.readFile('log.txt', 'utf8', (err, log) => {
          if (err) return callback(err);
          const newLog = log + 'nProcessing complete';
          fs.writeFile('log.txt', newLog, (err) => {
            callback(err, 'Done');
          });
        });
      });
    });
  });
}

Смотришь на это — и удивление пиздец. Чистая хитрая жопа, где потеряться проще простого. Одна ошибка — и всё, приехали.

2. Библиотеки-костыли: Чтобы как-то выжить, народ хватался за библиотеку async.js. Это как попытка навести порядок в бардаке с помощью ещё одного бардака, но покрасивше.

const async = require('async');

async.waterfall([
  (callback) => fs.readFile('data.json', 'utf8', callback),
  (config, callback) => {
    const configObj = JSON.parse(config);
    fs.readFile(configObj.inputFile, 'utf8', callback);
  },
  (data, callback) => {
    const processed = data.toUpperCase();
    fs.writeFile('output.txt', processed, (err) => callback(err, processed));
  }
], (err, result) => {
  if (err) console.error('Error:', err);
  else console.log('Result:', result);
});

Лучше, конечно, но всё равно чувствуешь себя каким-то полупидором, который прыгает через обручи. Доверия к этому коду — ебать ноль.

3. Эпоха промисов: Потом пришли нативные промисы из ES6. Ну, вроде прогресс, структура появилась. Но эти бесконечные цепочки .then() и .catch()... Бля буду, опять писать овердохуища текста.

const { readFile, writeFile } = require('fs').promises; // Используем fs.promises

function processWithPromises() {
  return readFile('data.json', 'utf8')
    .then(config => {
      const configObj = JSON.parse(config);
      return readFile(configObj.inputFile, 'utf8');
    })
    .then(data => {
      const processed = data.toUpperCase();
      return writeFile('output.txt', processed).then(() => processed);
    })
    .then(result => {
      console.log('Success:', result);
      return result;
    })
    .catch(err => {
      console.error('Failed:', err);
      throw err;
    });
}

Чувак, смотри: вроде логика есть, но глаза разбегаются. Каждый раз думаешь: "А этот then к чему относится? А ошибка где поймается?" Терпения ноль, ебать.

А потом случилась магия — async/await. И это, блядь, реально стало революцией. Всё просто: пишешь код почти как синхронный, но вся асинхронная мощь остаётся. И ошибки ловить через try/catch — красота же!

// Тот же функционал с async/await
async function processWithAsyncAwait() {
  try {
    const config = await readFile('data.json', 'utf8');
    const configObj = JSON.parse(config);
    const data = await readFile(configObj.inputFile, 'utf8');
    const processed = data.toUpperCase();
    await writeFile('output.txt', processed);
    console.log('Success:', processed);
    return processed;
  } catch (err) {
    console.error('Failed:', err);
    throw err;
  }
}

Читаешь сверху вниз — и всё понятно, как божий день. Никаких пирамид, никаких цепочек. Просто ждём результат и идём дальше. Переход на эту штуку сэкономил столько нервов и времени, что не передать. Количество ошибок, связанных с "а что когда выполнится", упало в разы. Рефакторить и дебажить стало в кайф, а не как раньше — с молитвой и крепким словцом.