Объект process в Node.js: argv, env, память и выход
✓ Все примеры выполнены на Node.js v22.23.1 (LTS), июль 2026
Содержание
process доступен в любом файле без импорта — он один из глобальных объектов Node — и знает о процессе всё: с какими аргументами его запустили, какие переменные окружения видны, сколько памяти занято и как корректно завершиться. Разберём то, что нужно на практике.
Аргументы командной строки
process.argv — массив, где первые два элемента заняты всегда:
console.log('argv[0] (движок) :', process.argv[0].split(/[\\/]/).pop());
console.log('argv[1] (скрипт) :', process.argv[1].split(/[\\/]/).pop());
console.log('argv[2+] (аргументы) :', process.argv.slice(2));
argv[0] (движок) : node.exe
argv[1] (скрипт) : process-examples.mjs
argv[2+] (аргументы) : [ '--port', '8080', '--verbose' ]
Отсюда вечное process.argv.slice(2): свои аргументы начинаются с третьего элемента.
parseArgs вместо ручного разбора
Разбирать --port 8080 циклом по массиву больше не нужно — в node:util есть штатный парсер.
import { parseArgs } from 'node:util';
const { values } = parseArgs({
args: ['--port', '8080', '--verbose'],
options: {
port: { type: 'string' },
verbose: { type: 'boolean' },
},
});
console.log('parseArgs ->', values);
parseArgs -> [Object: null prototype] { port: '8080', verbose: true }
Он понимает --flag, --key value, --key=value и короткие псевдонимы. Для CLI средней сложности этого хватает: commander и yargs нужны, только когда появляются подкоманды и генерация справки.
Переменные окружения: всё строки
process.env хранит только строки. Это источник тихих багов:
process.env.PORT_NUM = 8080;
console.log('тип значения :', typeof process.env.PORT_NUM);
console.log('во что превратилось :', JSON.stringify(process.env.PORT_NUM));
console.log('несуществующая перем. :', process.env.NO_SUCH_VAR);
тип значения : string
во что превратилось : "8080"
несуществующая перем. : undefined
Число 8080 записалось строкой "8080". Отсюда два правила:
- Приводите типы явно:
const port = Number(process.env.PORT) || 3000; - Осторожно с булевыми: строка
"false"истинна.if (process.env.DEBUG)сработает и приDEBUG=false. Сравнивайте явно:process.env.DEBUG === 'true'.
Несуществующая переменная — undefined, а не ошибка. Поэтому опечатка в имени тихо превращается в значение по умолчанию.
cwd: откуда считаются относительные пути
console.log('process.cwd() :', process.cwd());
console.log('import.meta.dirname :', import.meta.dirname);
console.log('совпадают? :', process.cwd() === import.meta.dirname);
process.cwd() : _tools/verify
import.meta.dirname : _tools/verify
совпадают? : true
В нашем запуске они совпали — но лишь потому, что скрипт запускали из его же папки. cwd() — это каталог, откуда вызвали node, а не тот, где лежит файл. Запустите тот же скрипт из другой директории — и все относительные пути в fs поедут.
Правило: пути к файлам, лежащим рядом с кодом, стройте от import.meta.dirname. От cwd() — только то, что пользователь указал сам. Подробнее про пути и про то, почему их нельзя склеивать конкатенацией, — в разборе модуля fs.
Память
const m = process.memoryUsage();
rss : 42.9 МБ (весь процесс)
heapTotal : 5.8 МБ (выделено под кучу)
heapUsed : 4.9 МБ (реально занято)
external : 1.8 МБ (C++ объекты, Buffer)
uptime : 0.03 с
| Поле | Что показывает |
|---|---|
rss |
вся память процесса в ОЗУ — то, что видно в диспетчере задач |
heapTotal |
сколько V8 выделил под кучу |
heapUsed |
сколько из неё реально занято объектами |
external |
память вне кучи V8: буферы, C++ объекты |
Практическая заметка: искать утечки по heapUsed в одной точке бесполезно — цифра прыгает от работы сборщика мусора. Мы на эти грабли наступали при замере потоков: heapUsed показал прямо противоположное реальности, и достоверные числа дал только rss с принудительной сборкой мусора.
Как завершать процесс
process.exit() обрывает процесс немедленно. Всё, что не успело записаться, теряется:
// ❌ асинхронная запись может не дойти до диска
appendFile('audit.log', 'важная запись\n');
process.exit(1);
// ✅ говорим, каким кодом хотим выйти, и даём процессу закончить
process.exitCode = 1;
Разница принципиальная: exitCode — это заявка. Node доработает текущие операции, отдаст незавершённые ответы и выйдет с нужным кодом сам. exit() — рубильник.
Тот же код возврата читает и родитель, если ваш скрипт запущен как дочерний процесс: именно по нему execFile решает, считать ли запуск неудачным.
Разница видна на замере. Скрипт с process.exit(0):
setTimeout(() => console.log('таймер отработал'), 10);
process.on('exit', () => console.log('обработчик exit'));
process.exit(0);
console.log('эта строка не выполнится');
вывод: "обработчик exit"
Таймер не отработал. Ни строки после exit(), ни отложенной работы — процесс срубили на месте.
Тот же скрипт, но через exitCode:
setTimeout(() => console.log('таймер отработал'), 10);
process.on('exit', (code) => console.log('обработчик exit, код', code));
process.exitCode = 3;
console.log('дошли до конца скрипта');
дошли до конца скрипта
таймер отработал
обработчик exit, код 3
код возврата: 3
Вся запланированная работа доделана, и код возврата всё равно нужный. Практическое правило: process.exit() в прикладном коде почти всегда ошибка. Он уместен ровно там, где вы сознательно бросаете всё: например, критический сбой при старте.
-
1
Оркестратор шлёт
SIGTERMdocker stop, systemd, Kubernetes. И даёт несколько секунд -
2
server.close()Новые соединения не принимаем, текущие дорабатывают -
3
Дозакрыть ресурсыБаза, очередь, файлы. Здесь
awaitещё работает -
4
process.exitCode = 0Неexit(): даём циклу опустеть самому -
5
Не успели —
SIGKILLЧерез таймаут оркестратора. Перехватить нельзя
Обработчик exit обязан быть синхронным
Ловушка, на которую натыкаются при попытке «дописать лог перед выходом»: в момент exit событийный цикл уже остановлен, поэтому ничего асинхронного там не выполнится.
process.on('exit', () => {
setTimeout(() => console.log('асинхронное в exit'), 0);
console.log('синхронное в exit');
});
вывод: "синхронное в exit"
setTimeout внутри exit не сработал — и не сработает никогда. Отправить метрику, дописать файл через await, закрыть соединение с базой в этом обработчике невозможно. Всё это делают заранее — по сигналу.
Graceful shutdown: закрыться, не роняя клиентов
Когда оркестратор (systemd, Docker, Kubernetes) останавливает приложение, он посылает SIGTERM и ждёт. Если процесс не завершится сам — через несколько секунд прилетит SIGKILL, который уже не перехватить. Задача — успеть закрыться корректно.
const server = createServer((req, res) => res.end('ok'));
await new Promise((r) => server.listen(0, '127.0.0.1', r));
console.log('сервер поднят');
const shutdown = (signal) => {
console.log('получен', signal, '- закрываем');
server.close(() => { // перестаём принимать новые соединения
console.log('соединения дозакрыты, выходим');
process.exitCode = 0; // не exit(): даём доработать
});
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
сервер поднят
получен SIGTERM - закрываем
соединения дозакрыты, выходим
Ключевое здесь — server.close() не рвёт текущие ответы: он перестаёт принимать новые соединения и ждёт, пока доработают уже начатые. Клиент, которому прямо сейчас отдаётся ответ, его дополучит.
Что стоит знать про сигналы:
| Сигнал | Откуда | Перехватывается |
|---|---|---|
SIGINT |
Ctrl+C в терминале | да |
SIGTERM |
docker stop, systemd, Kubernetes |
да |
SIGKILL |
kill -9, таймаут оркестратора |
нет, никогда |
⚠️ Оговорка про Windows: там сигналов в POSIX-смысле нет. SIGINT по Ctrl+C работает, а SIGTERM извне процессу не доставляется — этот пример мы проверяли на Windows через process.emit('SIGTERM'), что проверяет саму логику обработчика, но не доставку. В продакшене на Linux всё работает штатно; локально на Windows не удивляйтесь, что taskkill уходит мимо обработчика.
И вторая практическая деталь: на shutdown обычно вешают таймер-страховку. Если за 10 секунд закрыться не удалось (зависшее соединение), процесс выходит принудительно — иначе оркестратор всё равно пришлёт SIGKILL, но уже без вашего лога.
Коды по соглашению: 0 — успех, любое другое число — ошибка. Именно на них смотрят CI, systemd и Docker, чтобы понять, упал ли процесс.
Смежные темы: глобальные объекты — что ещё доступно без импорта; дочерние процессы — как запускать программы и читать их код возврата; HTTP-сервер — что именно закрывать при shutdown. Полный список — в справочнике по Node.js.
Частые вопросы
Чем `process.stdout.write` отличается от `console.log`?
console.log добавляет перевод строки и умеет форматировать объекты. stdout.write пишет ровно то, что дали, — нужен для прогресс-баров и вывода без разделителей.Почему процесс не завершается, хотя код кончился?
setInterval, открытое соединение с базой. Таймеру помогает unref(), остальное нужно закрывать явно.Как поймать Ctrl+C?
process.on('SIGINT', ...). Учтите, что поддержка сигналов различается между Linux и Windows: часть сигналов на Windows эмулируется, а часть недоступна.Откуда `process.env` берёт значения?
.env сами по себе не читаются: с Node 20 есть встроенный флаг --env-file=.env, иначе нужен пакет вроде dotenv.