Работа с файлами в Node.js: что нужно знать до начала
Три способа читать и писать файлы в Node.js, разница между синхронным и асинхронным API и почему промисы удобнее колбэков. С проверками на каждом шаге.

К концу этого материала у вас будет рабочий скрипт, который читает текстовый файл, дописывает в него строку и создаёт папку, если её нет. На это уйдёт минут двадцать. Никаких сторонних пакетов не нужно: всё, что понадобится, встроено в Node.js через модуль fs. Разберёмся, почему у одного и того же метода три разных варианта, какой из них брать по умолчанию и где новичок обычно спотыкается.
Что нужно иметь до начала
- Node.js 18 или новее. Проверьте командой
node -vв терминале — должна вывестись строка вродеv20.11.0. Всё в статье работает на 18.x и 20.x; вариант с промисами доступен начиная с Node.js 14. - Текстовый редактор. Подойдёт VS Code или любой другой.
- Папка для экспериментов. Создайте пустой каталог, например
fs-demo, и работайте внутри него. Так вы не удалите ничего лишнего.
Скрипты запускаются командой node имя_файла.js из той же папки. Никакого npm install не потребуется.
Три лица одного метода
Почти у каждой операции с файлами в fs есть три варианта. Это главное, что путает в начале.
- Синхронный — имя заканчивается на
Sync, напримерreadFileSync. Останавливает выполнение программы, пока файл не прочитается. - Асинхронный с колбэком —
readFileизnode:fs. Принимает функцию, которая вызовется, когда операция закончится. - Асинхронный с промисом —
readFileизnode:fs/promises. Возвращает промис, работает сasync/await.
По умолчанию берите промисы из node:fs/promises. Синхронные методы блокируют весь процесс, а колбэки быстро превращают код в лестницу вложенных функций.
Синхронные версии оправданы в одном случае: короткий скрипт, который запустился, сделал дело и завершился. Внутри веб-сервера, обрабатывающего запросы, readFileSync заморозит все остальные запросы на время чтения — это ошибка, которую находят уже под нагрузкой.
Читаем файл
Создайте рядом со скриптом файл data.txt с любым текстом. Затем вариант с промисами:
// read.js — Node.js 18+
import { readFile } from 'node:fs/promises';
try {
// Без указания кодировки вернётся Buffer, а не строка
const text = await readFile('data.txt', 'utf8');
console.log(text);
} catch (err) {
console.error('Не удалось прочитать файл:', err.message);
}
Запустите: node read.js. В терминале должно появиться содержимое data.txt. Если вместо текста вы видите что-то вроде <Buffer 50 72 69> — вы забыли передать 'utf8' вторым аргументом. Node вернул сырые байты.
Верхнеуровневый await без обёртки в async-функцию работает только в ES-модулях. Чтобы файл считался модулем, назовите его с расширением .mjs или добавьте "type": "module" в package.json. Иначе получите SyntaxError: Cannot use import statement outside a module.
Тот же файл через CommonJS
Если менять тип модуля не хочется, вот вариант на привычном require:
// read.cjs — Node.js 18+
const { readFile } = require('node:fs/promises');
async function main() {
try {
const text = await readFile('data.txt', 'utf8');
console.log(text);
} catch (err) {
console.error('Не удалось прочитать файл:', err.message);
}
}
main();
Пишем и дописываем
writeFile перезаписывает файл целиком. Если файла нет — создаёт. Если есть — старое содержимое пропадает без предупреждения. Это второе место, где новички теряют данные.
// write.js — Node.js 18+
import { writeFile, appendFile } from 'node:fs/promises';
// Перезаписывает файл целиком
await writeFile('log.txt', 'Первая строка\n', 'utf8');
// Дописывает в конец, старое не трогает
await appendFile('log.txt', 'Вторая строка\n', 'utf8');
console.log('Готово');
После запуска в log.txt будут обе строки. Если запустить скрипт второй раз, writeFile снова затрёт файл до одной строки, а appendFile добавит третью. Разницу видно сразу.
Работаем с путями
Третья частая ошибка — относительные пути. readFile('data.txt') ищет файл не рядом со скриптом, а в каталоге, откуда вы запустили node. Запустите скрипт из другой папки — получите ENOENT: no such file or directory.
Чтобы путь считался от расположения самого файла, соберите его через модуль path:
// paths.js — Node.js 18+
import { readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// В ES-модулях нет __dirname, восстанавливаем его
const __dirname = dirname(fileURLToPath(import.meta.url));
const filePath = join(__dirname, 'data.txt');
const text = await readFile(filePath, 'utf8');
console.log(text);
В CommonJS переменная __dirname доступна сразу, восстанавливать её не нужно. join собирает путь правильно и на Windows, и на Linux — не склеивайте пути строкой через слэш вручную.
Создаём папку, если её нет
Опция recursive: true у mkdir решает сразу две проблемы: создаёт все недостающие уровни вложенности и не падает с ошибкой, если папка уже существует.
// mkdir.js — Node.js 18+
import { mkdir, writeFile } from 'node:fs/promises';
await mkdir('output/reports', { recursive: true });
await writeFile('output/reports/report.txt', 'Отчёт готов\n', 'utf8');
console.log('Папка и файл созданы');
Без recursive: true вызов mkdir('output/reports') упадёт, если папки output ещё нет.
Синхронный или асинхронный: короткая таблица
| Ситуация | Что брать |
|---|---|
| Веб-сервер, много одновременных запросов | fs/promises, только асинхронно |
| Одноразовый скрипт, читает конфиг при старте и завершается | Можно readFileSync |
| Новый код без legacy | fs/promises с async/await |
| Старый проект на колбэках | fs с колбэками, чтобы не смешивать стили |
Если не получилось
SyntaxError: Cannot use import statement outside a module— вы используетеimport, но файл считается CommonJS. Переименуйте в.mjsили добавьте"type": "module"вpackage.json.ENOENT: no such file or directory— файла нет по указанному пути. Чаще всего дело в относительном пути: запустите скрипт из правильной папки или соберите путь черезpath.joinи__dirname.EACCES: permission denied— нет прав на чтение или запись. Проверьте владельца и права файла; не запускайте подsudo, пока не поняли причину.- Вместо текста
<Buffer ...>— не передали кодировку. Добавьте'utf8'вторым аргументом вreadFile. await is only valid in async functions— вы в CommonJS-файле или внутри обычной функции. Оберните код вasync functionили переведите файл в ES-модуль.
Что дальше
Когда основы улягутся, посмотрите в сторону трёх вещей. Потоки (createReadStream, createWriteStream) — для файлов, которые не влезают в память целиком: их читаешь кусками, а не загружаешь одним readFile. Проверка существования: не проверяйте файл через fs.access перед чтением — просто читайте и ловите ошибку в try/catch, иначе между проверкой и чтением файл может исчезнуть. И работа с JSON: readFile вернёт строку, которую нужно прогнать через JSON.parse в отдельном try/catch, потому что битый JSON — отдельный источник ошибок. Полный список методов fs смотрите в официальной документации для вашей версии.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: Node.js File system (fs) documentation, Node.js Path module documentation
Частые вопросы
У меня Node.js 16, всё это будет работать?
Чем отличается fs от fs/promises?
Почему readFile возвращает Buffer, а не строку?
Стоит ли проверять, существует ли файл, перед чтением?
Что делать, если код падает с ENOENT, хотя файл точно есть?
Когда синхронные методы вроде readFileSync допустимы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.