Работа с файлами в Node.js: частые ошибки новичков
Разбираем, почему падает чтение файла, куда исчезают ваши пути и когда синхронный вызов вешает весь сервер. Практический разбор ошибок, которые совершают почти все на старте.

Первое, обо что спотыкается почти каждый новичок в Node.js, — файл читается, а данные приходят не туда или не приходят вовсе. Код выглядит правильным, но в консоли то ENOENT, то <Buffer 48 65 6c ...> вместо текста, то сервер намертво зависает на пару секунд. Разберём типичные ошибки работы с модулем fs и покажем, как их избежать. Всё проверено на Node.js 20.x и 22.x.
Модуль fs предлагает три стиля работы: колбэки (fs.readFile), синхронные версии (fs.readFileSync) и промисы (fs.promises или node:fs/promises). Смешивать их в одном коде — уже половина проблем. Начнём с самой частой.
Ошибка 1. Синхронные вызовы в обработчике запросов
Новичок находит fs.readFileSync — он короткий и возвращает результат сразу, без колбэков. И ставит его прямо в обработчик HTTP-запроса. На одном пользователе всё летает. Под нагрузкой сервер начинает отвечать рывками.
Причина: Node.js обрабатывает запросы в одном потоке1. Синхронное чтение блокирует этот поток целиком — пока файл читается с диска, ни один другой запрос не обслуживается.
// Плохо: блокирует весь сервер на время чтения
import { createServer } from 'node:http';
import { readFileSync } from 'node:fs';
createServer((req, res) => {
const html = readFileSync('./page.html', 'utf8');
res.end(html);
}).listen(3000);
// Хорошо: асинхронное чтение не блокирует поток
import { createServer } from 'node:http';
import { readFile } from 'node:fs/promises';
createServer(async (req, res) => {
try {
const html = await readFile('./page.html', 'utf8');
res.end(html);
} catch (err) {
res.statusCode = 500;
res.end('Ошибка чтения файла');
}
}).listen(3000);
Синхронные версии уместны только при старте приложения: прочитать конфиг один раз до того, как сервер начал принимать запросы. В горячем пути — никогда.
Правило простое: если код выполняется в ответ на запрос пользователя, синхронного fs-вызова там быть не должно. Sync — только на этапе инициализации.
Ошибка 2. Относительные пути от «текущей папки»
Код читает ./config.json, работает при запуске из корня проекта и ломается с ENOENT, стоит запустить его из другой директории или через systemd. Дело в том, что относительный путь считается не от файла со скриптом, а от рабочего каталога процесса — того, что вернёт process.cwd(). А он зависит от того, откуда вы запустили node.
Чтобы путь был привязан к файлу, а не к месту запуска, стройте его от расположения самого модуля.
// ES-модули (package.json: "type": "module")
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const configPath = join(__dirname, 'config.json');
const config = JSON.parse(await readFile(configPath, 'utf8'));
В CommonJS-модулях переменные __dirname и __filename доступны сразу, без импортов. В ES-модулях их нет — их и восстанавливает конструкция выше. Начиная с Node.js 20.11 в ES-модулях также есть import.meta.dirname, что делает то же самое короче; проверьте вашу версию, прежде чем на него полагаться.
И никогда не склеивайте пути через + и слэши вручную — используйте path.join. Он сам подставит правильный разделитель и уберёт лишние.
Ошибка 3. Забытая кодировка — приходит Buffer вместо строки
Вызвали readFile без второго аргумента и удивляетесь, почему в консоли <Buffer 7b 0a ...>. Без указания кодировки fs возвращает сырой Buffer — массив байтов. Это не баг, а поведение по умолчанию.
- Нужен текст — передайте
'utf8':await readFile(path, 'utf8'). - Нужны бинарные данные (картинка, архив) — оставляйте
Buffer, кодировку не указывайте.
Частая западня — сразу после чтения делать JSON.parse над буфером. Работает случайно: JSON.parse приводит буфер к строке через toString(), но полагаться на это не стоит. Указывайте 'utf8' явно.
Ошибка 4. Проверка «файл существует», а потом чтение
Логика кажется разумной: сначала fs.existsSync, если файл есть — читаем. На деле это лишний вызов и потенциальная гонка: между проверкой и чтением файл могут удалить. Правильнее просто попытаться прочитать и обработать ошибку.
import { readFile } from 'node:fs/promises';
async function loadOrDefault(path) {
try {
return await readFile(path, 'utf8');
} catch (err) {
// ENOENT — файла нет, возвращаем значение по умолчанию
if (err.code === 'ENOENT') return '';
// Другие ошибки (нет прав, это каталог) пробрасываем дальше
throw err;
}
}
Смотрите на err.code, а не на текст сообщения. Код стабилен между версиями, текст может меняться и зависит от локали. ENOENT — нет файла, EACCES — нет прав, EISDIR — по пути каталог, а не файл.
Ошибка 5. Проглоченные ошибки в колбэках
В колбэк-стиле первый аргумент — всегда ошибка. Забыли её проверить — и при сбое читаете undefined вместо данных, а причину не видите.
// Плохо: если чтение упало, data будет undefined, а ошибку никто не увидел
import { readFile } from 'node:fs';
readFile('./data.json', 'utf8', (err, data) => {
const parsed = JSON.parse(data); // рухнет с невнятным сообщением
});
// Хорошо
readFile('./data.json', 'utf8', (err, data) => {
if (err) {
console.error('Не удалось прочитать файл:', err.code);
return;
}
const parsed = JSON.parse(data);
});
Проще перейти на node:fs/promises и try/catch — там ошибку невозможно случайно пропустить, она сама всплывёт как исключение.
Ошибка 6. Чтение большого файла целиком в память
Лог на 2 ГБ через readFile — и процесс падает с ошибкой памяти или упирается в лимит размера буфера. Весь файл загружается в оперативную память разом. Для больших файлов или потоковой обработки используйте стримы.
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const stream = createReadStream('./huge.log', 'utf8');
const lines = createInterface({ input: stream });
let count = 0;
for await (const line of lines) {
if (line.includes('ERROR')) count++;
}
console.log('Строк с ошибкой:', count);
Здесь файл читается кусками, в памяти в каждый момент — одна строка, а не весь файл. Для копирования большого файла есть отдельный fs.copyFile, а для соединения потоков — stream.pipeline из node:stream/promises.
Сравнение трёх стилей
| Стиль | Импорт | Когда использовать | Риск |
|---|---|---|---|
| Синхронный | node:fs (Sync-версии) | Инициализация, CLI-скрипты | Блокирует поток, нельзя в обработчике запроса |
| Колбэки | node:fs | Легаси-код, простые задачи | Легко проглотить ошибку, вложенность |
| Промисы | node:fs/promises | Основной выбор для нового кода | Забытый await вернёт Promise вместо данных |
Для нового кода выбирайте промисы. Синхронные версии — только на старте приложения. Колбэки оставьте для чужого кода, который уже так написан.
Если не получилось
- Error: ENOENT: no such file or directory — путь считается не оттуда, откуда вы думаете. Выведите
console.log(process.cwd())и стройте путь черезpath.join(__dirname, ...). - В консоли
<Buffer ...>вместо текста — забыли'utf8'вторым аргументом. - Данные
undefined, ошибок нет — в промис-стиле забылиawait: переменной присвоился Promise. В колбэк-стиле не проверили первый аргумент. - Сервер отвечает рывками под нагрузкой — где-то в горячем пути синхронный
fs-вызов. Найдите всеSyncи уберите из обработчиков. - EACCES: permission denied — у процесса нет прав на файл или каталог. Проверьте владельца и права, под которыми запущен Node.
Что делать дальше
Переведите весь новый код на node:fs/promises и включите линтер-правило против синхронных вызовов в обработчиках. Следующий шаг — освоить стримы и stream/promises: как только файлы перестают помещаться в память, без них не обойтись. И заведите привычку смотреть на err.code, а не на текст ошибки — это спасёт код при обновлении Node.
1 Точнее, весь ваш JavaScript выполняется в одном потоке событийного цикла. Ввод-вывод Node выносит на пул потоков libuv, поэтому асинхронное чтение не блокирует основной поток, а синхронное — блокирует, потому что ждёт результат прямо в нём.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: Node.js — File system (документация модуля fs), Node.js — Common system errors (коды ошибок)
Частые вопросы
У меня Node.js 18, конструкция с import.meta.dirname не работает
Можно ли вообще использовать fs.readFileSync?
Чем node:fs/promises отличается от fs.promises?
Почему JSON.parse иногда работает над буфером без указания кодировки?
Как читать файл построчно, если он большой?
Нужно ли проверять существование файла перед чтением?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.