Объект 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() в прикладном коде почти всегда ошибка. Он уместен ровно там, где вы сознательно бросаете всё: например, критический сбой при старте.

Graceful shutdown: как закрыться, не роняя клиентов
  1. 1 Оркестратор шлёт SIGTERMdocker stop, systemd, Kubernetes. И даёт несколько секунд
  2. 2 server.close()Новые соединения не принимаем, текущие дорабатывают
  3. 3 Дозакрыть ресурсыБаза, очередь, файлы. Здесь await ещё работает
  4. 4 process.exitCode = 0Не exit(): даём циклу опустеть самому
  5. 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.