fs.appendFile(): как дописать текст в конец файла
✓ Все примеры выполнены на Node.js v22.23.1 (LTS), июль 2026
Содержание
Дописать строку в конец файла — задача на одну строку кода, и fs.appendFile из модуля fs её решает. Проблемы начинаются, когда этой одной строкой пишут журнал в цикле.
Как дописать в конец файла
import { appendFile, readFile } from 'node:fs/promises';
await appendFile('app.log', 'первая строка\n');
await appendFile('app.log', 'вторая строка\n');
console.log(await readFile('app.log', 'utf8'));
первая строка
вторая строка
Перевод строки \n нужно ставить самому — appendFile дописывает ровно то, что передали, без разделителей.
appendFile создаёт файл сам
Проверять существование заранее не нужно: если файла нет, он будет создан.
console.log('существовал до :', existsSync(fresh));
await appendFile(fresh, 'создан appendFile\n');
console.log('существует после:', existsSync(fresh));
существовал до : false
существует после: true
А вот каталог appendFile не создаст. Если путь logs/app.log, а папки logs нет — получите ENOENT. Каталог создаётся отдельно: mkdir(dir, { recursive: true }).
appendFile против writeFile
writeFile затирает файл целиком, appendFile дописывает в конец. Проверим:
await writeFile(w, 'первый\n');
await writeFile(w, 'второй\n');
console.log('после двух writeFile:', await readFile(w, 'utf8'));
await appendFile(w, 'третий\n');
console.log('после appendFile :', await readFile(w, 'utf8'));
после двух writeFile : "второй\n"
после appendFile : "второй\nтретий\n"
Первый writeFile полностью стёрт вторым — от строки «первый» не осталось ничего.
Секрет: это один и тот же вызов
appendFile не делает ничего особенного — под капотом это открытие файла с флагом 'a' (append). То же самое доступно и через writeFile:
await writeFile(w2, 'a\n');
await writeFile(w2, 'b\n', { flag: 'a' }); // ← дописывает, а не затирает
writeFile с flag:'a' : "a\nb\n"
Практический вывод: если вы уже используете writeFile и нужно поведение «дописать» — не ищите другую функцию, добавьте флаг.
Полезные флаги:
| Флаг | Что делает |
|---|---|
'w' |
создать или затереть (по умолчанию у writeFile) |
'a' |
создать или дописать (по умолчанию у appendFile) |
'wx' |
создать, но упасть с EEXIST, если файл уже есть |
'ax' |
дописать, но упасть, если файла ещё нет |
Флаг 'wx' — способ реализовать «создать, только если не существует» без гонки: проверку делает сама файловая система, а не ваш код.
Цена appendFile в цикле
Вот здесь и кроется подвох. Каждый вызов appendFile открывает файл, пишет и закрывает его. На одной записи это незаметно, на двух тысячах — уже нет.
Сравним с открытием дескриптора один раз:
// ❌ 2000 циклов открыть-записать-закрыть
for (let i = 0; i < 2000; i++) {
await appendFile(many, `строка ${i}\n`);
}
// ✅ открыли один раз, пишем, закрыли
const fh = await open(one, 'a');
for (let i = 0; i < 2000; i++) {
await fh.write(`строка ${i}\n`);
}
await fh.close();
2000× appendFile : 433 мс
2000× write в дескриптор : 64 мс
разница : 6.8x
Данные таблицей
| Показатель | Время |
|---|---|
| 2000× appendFile | 433 мс |
| 2000× write в дескриптор | 64 мс |
Источник: собственный замер, Node.js v22.23.1
Не забудьте fh.close() — иначе дескриптор утечёт. Надёжнее обернуть в try/finally.
Когда что использовать:
- разовая запись —
appendFile, он ровно для этого и сделан; - журнал, много записей подряд — открытый дескриптор через
open(path, 'a')либо потокcreateWriteStream(path, { flags: 'a' }); - продакшен-логи — готовая библиотека вроде
pino: она решает и буферизацию, и ротацию.
Права доступа: аргумент mode
Третий, почти неизвестный аргумент — mode, права создаваемого файла. По умолчанию 0o666 (чтение и запись всем), которые ОС ещё урежет по umask.
await appendFile('secrets.log', 'строка\n', { mode: 0o600 }); // только владелец
Это важно ровно там, где в журнал попадает что-то чувствительное. mode действует только при создании файла: если файл уже есть, права не изменятся — для этого нужен chmod. И на Windows права POSIX не применяются, аргумент там практически бесполезен.
Если запись падает с EACCES — дело не в Node: у процесса нет прав на файл или каталог. Проверять надо владельца и права каталога, а не код.
Чего appendFile не делает: ротация
Главная причина, по которой appendFile не годится для журнала в продакшене, — файл растёт бесконечно. Через месяц у вас гигабайтный app.log, который нечем открыть, а место на диске кончилось.
appendFile про это ничего не знает. Варианты:
- системная ротация —
logrotateна Linux: приложение просто пишет в файл, ОС режет его по расписанию. Нюанс: после ротации нужно переоткрыть дескриптор, иначе вы продолжите писать в удалённый файл (поэтомуlogrotateумеет слать сигнал приложению); - библиотека —
pinoс транспортом ротации: она же решает буферизацию, которую вы иначе будете писать руками; - писать в stdout — и вообще не заниматься файлами. Так устроены контейнеры: приложение пишет в
process.stdout, а сбор и ротацию делает Docker или systemd. Для нового кода это лучший вариант по умолчанию.
Если журнал пишет HTTP-сервер, помните про цену синхронной записи: appendFileSync в обработчике запроса останавливает весь процесс — мы это замеряли.
Другие разборы файловой системы и платформы — в справочнике по Node.js.
Практический вывод: appendFile хорош для разовой дозаписи. Как только речь про журнал, который живёт долго, — вопрос уже не в функции записи, а в том, кто отвечает за размер файла.
Что было на этой странице в 2017 году
Первая версия этого разбора вышла 13 августа 2017 года. Пример был такой:
const fs = require('fs');
fs.appendFile('message.txt', 'Данные для добавления', 'utf8', (err) => {
if (err) throw err;
console.log('Данные были добавлены в конец файла!');
});
Приметы времени — require и колбэк с if (err) throw err. Промис-версии тогда не было: fs.promises появился только в Node 10, в 2018-м.
А вот что в той версии было и остаётся точным — документация аргументов: mode по умолчанию 0o666, флаг по умолчанию 'a'. И отдельное примечание, которое стоит того, чтобы его повторить дословно:
«Если в качестве файла указан файловый дескриптор, он не будет закрыт автоматически.»
Это ровно та ловушка, из-за которой в цикле выше мы обязаны вызывать fh.close(). Восемь лет спустя ничего не изменилось: appendFile умеет принимать не только путь, но и открытый дескриптор — и в этом случае закрывать его придётся вам.
Про параллельную запись
appendFile открывает файл с флагом 'a', и на уровне ОС такая запись атомарна для небольших порций — строки от разных вызовов не перемешаются посимвольно. Но порядок строк при параллельных вызовах не гарантирован:
// порядок в файле может оказаться любым
await Promise.all([
appendFile(log, 'первая\n'),
appendFile(log, 'вторая\n'),
]);
Если порядок важен — пишите последовательно или через один дескриптор.
Частые вопросы
Чем `appendFile` отличается от `appendFileSync`?
readFile от readFileSync: синхронная версия блокирует событийный цикл целиком. Подробнее — в статье про readFile и readFileSync.Можно ли дописать в начало файла?
Почему файл пустой, хотя ошибок нет?
await процесс может завершиться раньше, чем запись дойдёт до диска.Как дописать `Buffer`, а не строку?
appendFile принимает и строку, и Buffer. Для строки кодировку можно задать третьим аргументом.