Пошук уроків, статей та іншого контенту
Розглянете глобальні обробники необроблених помилок і відхилених промісів у браузері та середовищі Node.js.
Глобальні обробники потрібні для двох основних завдань:
зафіксувати помилки, які не були оброблені локально;
виконати останні дії перед завершенням застосунку або втратою його працездатності.
Типові джерела проблем:
виняток у синхронному коді;
виняток у callback-функції;
проміс, що був відхилений без обробника;
помилка завантаження скрипта;
помилка в асинхронній операції, яку не передали через await або .catch().
Глобальний обробник не замінює локальну обробку. Він є останнім рівнем спостереження та аварійного завершення.
Якщо помилка означає, що стан процесу або застосунку більше не можна вважати надійним, продовження роботи може призвести до пошкодження даних. У такому випадку безпечніше завершити процес або перезавантажити застосунок.
window.onerrorwindow.onerror — спеціальний обробник помилок, що виникли у JavaScript-коді сторінки.
window.onerror = function (
message,
source,
line,
column,
error
) {
console.error("Глобальна помилка:", {
message,
source,
line,
column,
error
});
// Повернення true пригнічує стандартний вивід помилки браузером.
// Зазвичай цього робити не потрібно.
return false;
};Обробник отримує:
текст повідомлення;
URL джерела;
номер рядка;
номер стовпця;
об'єкт Error, якщо він доступний.
У сучасному коді частіше використовують addEventListener, оскільки він дає змогу реєструвати кілька обробників:
window.addEventListener("error", (event) => {
console.error("Помилка браузера:", {
message: event.message,
filename: event.filename,
lineNumber: event.lineno,
columnNumber: event.colno,
error: event.error
});Подія error також може виникати через помилки завантаження ресурсів:
window.addEventListener("error", (event) => {
const target = event.target;
if (target instanceof HTMLScriptElement) {
console.error("Не вдалося завантажити скрипт:", target.src);
} else if (target instanceof HTMLImageElement) {
console.error("Не вдалося завантажити зображення:", target.src);
}
}, true);Для помилок ресурсів потрібен режим захоплення подій (true), оскільки такі події не завжди спливають звичайним способом.
Якщо скрипт завантажено з іншого походження без належних CORS-заголовків, браузер може приховати деталі помилки. Замість повного stack trace можна отримати повідомлення на кшталт Script error.
Для доступу до деталей зазвичай потрібні:
атрибут crossorigin у тегу script;
відповідний заголовок Access-Control-Allow-Origin на сервері.
Навіть за правильної конфігурації не слід передавати на сервер конфіденційні значення з повідомлень помилок без попереднього очищення.
Винятки в async-функціях перетворюються на відхилені проміси. Якщо такий проміс не має обробника, браузер генерує подію unhandledrejection.
window.addEventListener("unhandledrejection", (event) => {
console.error("Необроблене відхилення промісу:", {
reason: event.reason,
promise: event.promise
});
// event.preventDefault() вимикає стандартний вивід у консоль.
// Використовуйте це лише якщо помилка справді вже залогована.
// event.preventDefault();
});Приклад:
async function loadProfile() {
throw new Error("Профіль недоступний");
}
loadProfile();
// Подія unhandledrejection виникне після завершення поточного циклу,
// якщо до промісу не буде додано обробник.Правильний локальний варіант:
loadProfile().catch((error) => {
console.error("Не вдалося завантажити профіль:", error);
});Порядок додавання обробника має значення:
const promise = Promise.reject(new Error("Тимчасова помилка"));
setTimeout(() => {
promise.catch((error) => {
console.error("Помилку оброблено із запізненням:", error);
});
}, 0);Спочатку браузер може згенерувати unhandledrejection, а згодом — rejectionhandled, коли обробник буде додано:
window.addEventListener("rejectionhandled", (event) => {
console.info("Для промісу пізніше додали обробник:", event.promise);
});Точний момент перевірки необробленого промісу залежить від середовища, але практично слід додавати .catch() або використовувати await у межах того самого логічного ланцюжка.
Глобальний обробник має:
нормалізувати значення помилки;
видалити або замаскувати секрети;
додати контекст: URL, версію застосунку, ідентифікатор сесії;
відправити подію на сервер спостереження;
не створити нову помилку всередині самого обробника.
function normalizeError(value) {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack
};
}
return {
name: typeof value,
message: String(value),
stack: undefined
};
}
function reportError(payload) {
try {
const body = JSON.stringify({
...payload,
url: location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
});
navigator.sendBeacon(
"/api/client-errors",
new Blob([body], { type: "application/json" })
);
} catch (reportingError) {
// Помилка логування не повинна ламати обробник помилки.
console.error("Не вдалося відправити звіт:", reportingError);
}
}
window.addEventListener("error", (event) => {
reportError({
type: "uncaught-error",
error: normalizeError(event.error ?? event.message),
source: event.filename,
line: event.lineno,
column: event.colno
});
});
window.addEventListener("unhandledrejection", (event) => {
reportError({
type: "unhandled-rejection",
error: normalizeError(event.reason)
});
});navigator.sendBeacon() зручний для невеликих фонових повідомлень, особливо під час завершення навігації. Однак сервер має обмежувати розмір запиту, перевіряти вхідні дані та не довіряти stack trace як структурованому формату.
У застосунках, де потрібна підтримка старіших браузерів або контроль над заголовками, замість sendBeacon() може використовуватися fetch() з опцією keepalive.
У Node.js глобальні події надходять через об'єкт process.
uncaughtExceptionПодія uncaughtException виникає, якщо виняток вийшов за межі всіх синхронних обробників.
process.on("uncaughtException", (error, origin) => {
console.error("Необроблений виняток:", {
origin,
name: error.name,
message: error.message,
stack: error.stack
});
process.exitCode = 1;
});Після uncaughtException не можна вважати стан процесу повністю надійним. Обробник призначений для:
синхронного або майже синхронного запису критичного логу;
закриття серверних з'єднань;
завершення процесу з ненульовим кодом.
Не слід перетворювати цей обробник на механізм продовження нормальної роботи:
process.on("uncaughtException", () => {
// Небезпечний підхід:
// процес може залишатися в пошкодженому стані.
});Зазвичай менеджер процесів або оркестратор запускає новий екземпляр після завершення старого.
uncaughtExceptionMonitoruncaughtExceptionMonitor викликається перед uncaughtException, але сам по собі не змінює стандартну поведінку Node.js.
Він корисний для моніторингу:
process.on("uncaughtExceptionMonitor", (error, origin) => {
console.error("Моніторинг необробленого винятку:", {
origin,
message: error instanceof Error ? error.message : String(error)
});
});Це зручно, коли інша частина програми або бібліотека вже встановлює uncaughtException, а додатковий компонент лише збирає діагностичні дані.
unhandledRejectionПодія unhandledRejection виникає, коли проміс відхилено і для нього не знайдено обробника у відповідний момент.
process.on("unhandledRejection", (reason, promise) => {
console.error("Необроблене відхилення промісу:", {
reason,
promise
});
});Поведінка Node.js щодо необроблених відхилень залежить від версії та параметрів запуску. У сучасних версіях необроблене відхилення зазвичай призводить до поведінки, близької до необробленого винятку. Покладатися на конкретний режим без явної конфігурації не слід.
Запуск із параметром --unhandled-rejections дає змогу явно вибрати режим, але політику краще визначати на рівні застосунку та середовища виконання.
rejectionHandledЯкщо обробник промісу додано із запізненням, Node.js може згенерувати rejectionHandled:
process.on("rejectionHandled", (promise) => {
console.warn("Для раніше необробленого промісу додано обробник:", promise);
});Цю подію корисно враховувати в системах моніторингу, щоб не трактувати кожен короткочасний unhandledRejection як остаточну помилку.
Наведений сервер:
обробляє необроблені винятки;
обробляє необроблені відхилення промісів;
реєструє події для моніторингу;
припиняє приймати нові з'єднання;
завершує процес із кодом 1.
const http = require("node:http");
const server = http.createServer((request, response) => {
if (request.url === "/crash") {
throw new Error("Критична помилка обробника запиту");
}
if (request.url === "/reject") {
Promise.reject(new Error("Необроблене відхилення промісу"));
response.end("Відхилення створено\n");
return;
}
response.end("Сервер працює\n");
});
let isShuttingDown = false;
function serializeError(value) {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack
};
}
return {
name: typeof value,
message: String(value)
};
}
function reportFatalError(type, value, metadata = {}) {
const record = {
type,
error: serializeError(value),
metadata,
timestamp: new Date().toISOString()
};
// У реальному застосунку тут можна синхронно записати критичний лог.
console.error(JSON.stringify(record, null, 2));
}
function shutdown(reason, error) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
reportFatalError(reason, error);
// Перестаємо приймати нові з'єднання.
server.close((closeError) => {
if (closeError) {
reportFatalError("shutdown-error", closeError);
}
process.exit(1);
});
// Не очікуємо безкінечно на з'єднання, що зависло.
setTimeout(() => {
process.exit(1);
}, 10_000).unref();
}
process.on("uncaughtExceptionMonitor", (error, origin) => {
reportFatalError("uncaught-exception-monitor", error, { origin });
});
process.on("uncaughtException", (error, origin) => {
shutdown("uncaught-exception", error);
console.error("Походження події:", origin);
});
process.on("unhandledRejection", (reason) => {
shutdown("unhandled-rejection", reason);
});
process.on("rejectionHandled", () => {
console.warn("Раніше необроблене відхилення отримало обробник");
});
server.listen(3000, () => {
console.log("Сервер слухає порт 3000");
});Запуск:
node server.jsЗвичайний запит:
curl http://localhost:3000/Запит до /crash демонструє необроблений виняток, а запит до /reject — необроблене відхилення промісу. Після аварійної події сервер завершується. У робочій системі його має запустити зовнішній менеджер процесів.
Глобальна помилка та штатне завершення — різні сценарії.
Для штатного завершення часто обробляють сигнали:
function shutdownGracefully(signal) {
console.log(`Отримано сигнал ${signal}`);
server.close(() => {
process.exit(0);
});
}
process.once("SIGTERM", () => shutdownGracefully("SIGTERM"));
process.once("SIGINT", () => shutdownGracefully("SIGINT"));Під час завершення варто:
припинити приймати нові запити;
завершити або скасувати активні операції;
закрити підключення до бази даних;
записати критичні логи;
мати обмеження часу очікування;
завершити процес із правильним кодом.
Не варто виконувати складні асинхронні дії після uncaughtException, якщо вони залежать від потенційно пошкодженого стану застосунку.
Корисний запис помилки зазвичай містить:
тип події: uncaught-error, unhandled-rejection, uncaught-exception;
name, message, stack;
ідентифікатор версії застосунку;
середовище виконання;
URL або маршрут;
ідентифікатор запиту;
безпечний технічний контекст;
час виникнення.
Не слід безпосередньо логувати:
паролі;
токени доступу;
cookies;
повні заголовки авторизації;
персональні дані;
великі об'єкти запитів і відповідей.
Причина відхилення промісу може бути не об'єктом Error, а будь-яким значенням:
Promise.reject("операція не виконана");
Promise.reject({ code: "NETWORK_FAILURE" });Тому функція серіалізації повинна безпечно обробляти рядки, числа, об'єкти та null.
Глобальний обробник не знає бізнес-контексту помилки. Він не може коректно вирішити, чи потрібно повторити запит, показати повідомлення користувачу або відкотити транзакцію.
Локально слід обробляти очікувані помилки, а глобально — фіксувати пропущені та критичні.
uncaughtExceptionПроцес може мати пошкоджений стан: частково змінені дані, незавершені транзакції, некоректні кеші або порушені інваріанти.
Безпечніша стратегія — коротке аварійне завершення і перезапуск процесу.
process.exit() одразуНегайний вихід може перервати запис логу, завершення відповіді або закриття з'єднань. Спочатку потрібно виконати мінімальне коректне завершення з тайм-аутом.
Наприклад, обробник намагається серіалізувати циклічний об'єкт або звертається до вже недоступного сервісу логування. У результаті первинна причина втрачається.
Код глобального обробника має бути коротким, захищеним і мати запасний шлях виводу.
unhandledrejection остаточною помилкоюОбробник може бути доданий із запізненням. Для точного моніторингу потрібно враховувати rejectionHandled і групувати події.
preventDefault()У браузері event.preventDefault() може вимкнути стандартний вивід у консоль. Якщо власне логування не спрацювало, інформація про помилку буде втрачена.
Пригнічуйте стандартну поведінку лише тоді, коли маєте надійний еквівалентний механізм.
Stack trace може містити параметри URL, імена файлів, внутрішні шляхи або інші чутливі значення. Перед відправленням на сервер дані потрібно нормалізувати та очищати.
Обробляйте очікувані помилки локально.
Реєструйте браузерні error та unhandledrejection.
У Node.js визначте політику для uncaughtException і unhandledRejection.
Не продовжуйте роботу процесу після критичного необробленого винятку.
Зробіть аварійне завершення ідемпотентним.
Закривайте ресурси з обмеженим тайм-аутом.
Відправляйте мінімально необхідні та очищені діагностичні дані.
Перевіряйте глобальні обробники в тестовому середовищі.
Налаштуйте зовнішній перезапуск Node.js-процесу.
Відокремлюйте спостереження за помилками від бізнес-логіки.
window.onerror і подія error обробляють необроблені помилки браузера.
unhandledrejection призначена для необроблених відхилень промісів у браузері та Node.js.
rejectionHandled сигналізує, що обробник було додано із запізненням.
У Node.js uncaughtException є ознакою потенційно ненадійного стану процесу.
uncaughtExceptionMonitor дає змогу спостерігати за критичними винятками без зміни основної поведінки.
Глобальні обробники потрібні для діагностики та аварійного завершення, а не для приховування помилок.
Критичні логи мають бути безпечними, короткими та стійкими до повторної помилки.
Після серйозного необробленого винятку Node.js-процес зазвичай слід завершити та перезапустити.