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 против одного дескриптора
Данные таблицей
Показатель Время
2000× appendFile 433 мс
2000× write в дескриптор 64 мс

Источник: собственный замер, Node.js v22.23.1

6.8x
Выигрыш одного дескриптора
433 мс → 64 мс на 2000 записей
Источник: собственный замер, Node v22.23.1
3
Операции ОС на каждый вызов
appendFile: открыть, записать, закрыть

Не забудьте 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. Для строки кодировку можно задать третьим аргументом.