fs/promises это версия модуля fs в Node на промисах. Его функции (readFile, writeFile, readdir, stat, mkdir, rm, rename и остальные) возвращают промис, а не принимают callback, поэтому вы ждёте их через await и ловите ошибки через try/catch. Импортируйте его через require('fs/promises') или import fs from 'fs/promises'.
Программа выводит [ 'write docs', 'ship it' ]: каждый await ждёт одну операцию, прежде чем выполнится следующая строка, поэтому дописывание всегда происходит после первой записи. В ES-модуле await работает на верхнем уровне; в CommonJS оберните код в функцию async.
Функции, которые нужны чаще всего
Имена совпадают с API на callback'ах, только без callback'а. Две из них подробно разобраны на страницах readFile и writeFile.
| Функция | Что делает | Чем разрешается |
|---|---|---|
readFile(path, 'utf8') | Читает файл целиком | Содержимым |
writeFile(path, data) | Создаёт или заменяет файл | undefined |
appendFile(path, data) | Дописывает в конец | undefined |
readdir(path) | Возвращает содержимое папки | Массивом имён |
stat(path) | Размер, даты, файл или папка | Объектом Stats |
mkdir(path, { recursive: true }) | Создаёт папки | Первой созданной папкой или undefined |
rename(from, to) | Перемещает или переименовывает | undefined |
rm(path, { recursive: true, force: true }) | Удаляет файл или папку | undefined |
Содержимое папки через readdir и stat
readdir возвращает только имена. Вызовите stat для каждого, чтобы узнать размер или отличить файлы от папок, или передайте { withFileTypes: true }, чтобы получить записи, которые уже знают свой тип.
Цикл выводит README.md 6 bytes, notes.txt 3 bytes и src folder. Для несуществующего пути stat отклоняется с ENOENT, поэтому он годится как быстрая проверка существования, когда заодно нужны подробности.
Переименование, перемещение и удаление
rename и переименовывает, и перемещает файл (в пределах одного диска). rm удаляет; для непустой папки ему нужен recursive: true, а force: true, чтобы не падать, если пути уже нет.
Файл переезжает из drafts в published под новым именем, а после rm в папке blog остаётся только published. Уберите force: true из второго rm, и он отклонится с ENOENT.
Ошибки через try/catch
Неудачная операция отклоняет свой промис. С await это отклонение становится исключением, которое ловится как любое другое. Поле code у ошибки говорит, что пошло не так.
Первый вызов не находит файла и возвращает []; второй выводит Ada. Коды, которые стоит знать: ENOENT (нет такого файла), EEXIST (уже существует), EISDIR (папка там, где ожидался файл), ENOTEMPTY (в папке ещё что-то есть) и EACCES (нет прав).
Промис, который вы не ждёте через await и не обрабатываете через .catch(), при ошибке становится необработанным отклонением, и Node завершается с ошибкой. Ждите каждый вызов или возвращайте его промис вызывающему коду, который его дождётся.
Чтение нескольких файлов сразу через Promise.all
Цикл for с await читает файлы один за другим. Когда чтения не зависят друг от друга, запустите их все и подождите один раз через Promise.all: диск будет работать над ними одновременно.
Promise.all сохраняет порядок входного массива, поэтому результат будет [ 'alpha', 'beta', 'gamma' ], какое бы чтение ни закончилось первым, а длина склеенной строки равна 14. Замените в массиве 'b.txt' на 'x.txt', и весь await отклонится с ENOENT.
Если нужны все результаты, даже когда часть чтений не удалась, используйте Promise.allSettled; оба метода описаны на странице про промисы.
Частые ошибки
Путаница между двумя модулями. require('fs').readFile ждёт callback; require('fs/promises').readFile возвращает промис. Вызов версии на callback'ах без callback'а выбрасывает The "cb" argument must be of type function.
Забытый await. const text = fs.readFile(...) это промис в ожидании, и text.split выбрасывает text.split is not a function.
Параллельная запись в один файл. Promise.all с несколькими записями в один путь может оставить перемешанное содержимое. Выполняйте их последовательно.
Часто задаваемые вопросы
Чем fs.promises отличается от fs/promises?
Ничем. require('fs').promises и require('fs/promises') это один и тот же объект. Путь fs/promises (или node:fs/promises) сегодня обычный способ его импортировать.
Есть ли existsSync в fs/promises?
Нет. В API на промисах есть access, который отклоняется, если пути нет, но обычно просто вызывают нужную операцию и ловят ENOENT. existsSync из обычного модуля fs по-прежнему подходит для быстрых проверок.
fs/promises медленнее, чем API на callback'ах?
Чуть-чуть, из-за накладных расходов промисов, но рядом со временем работы диска это незаметно. Выбирайте по читаемости; большая часть нового кода использует API на промисах.
Как превратить функцию fs с callback'ом в промис?
Это редко нужно, потому что fs/promises покрывает функции для файлов. Для других API на callback'ах util.promisify(fn) возвращает версию, которая возвращает промис.