
Отказоустойчивость (англ. resilience, fault tolerance) - это способность системы продолжать работу, несмотря на внутренние ошибки, сбои в зависимостях или непредвиденные ситуации.
С хорошей отказоустойчивостью интерфейс остаётся стабильным и понятным, пользователь получает предсказуемый и комфортный опыт, а сбои отдельных компонентов не приводят к сбоям всей системы. Но при этом отказоустойчивость не означает сохранение работоспособности системы любой ценой
В этой статье речь не будет идти о конкретных примерах реализации повышения отказоустойчивости. Понять то, что нужно подключать сервисы мониторинга ошибок вы можете и без меня
Хорошая отказоустойчивость начинается с мышления
Я хочу, чтобы эта фраза въелась вам в самую подкорку
Важно не просто латать ошибки по мере их появления, а комплексно подходить к решению — формировать правильное понимание, разрабатывать устойчивые подходы и строить систему, способную адекватно реагировать на возможные сбои.
Принципы описанные ниже в большинстве своем универсальные и подойдут к большому количеству сфер, даже вне области информационных технологий. Но моя основная опора будет на сферу веб-разработки
Это не догмы и не жёсткие правила, а образ мышления. Добиться абсолютной отказоустойчивости невозможно (да и стремиться к этому не всегда разумно).
Обо всём этом и даже больше подробнее пойдет далее
10 Принципов отказоустойчивости
0. Прежде чем заниматься отказоустойчивостью наведи порядок в своем коде
Отказоустойчивость должна защищать от реальных рискованных ситуаций, а не компенсировать ошибки и недочёты в плохом коде
Если базовый код хрупок и ненадёжен, никакие механизмы устойчивости не сделают систему по-настоящему стабильной - это всё равно что имея дырявое ведро думать над тем, что делать с лужами, вместо того чтобы изначально иметь целое ведро.
1. Сломаться может всё, в любой момент и в любом месте
Ошибки случаются. Это нормально. Ненормально - когда из-за этих ошибок пользователь начинает страдать: сталкивается с пустым экраном, сломанной вёрсткой, непонятным поведением или вовсе не может пользоваться приложением.
Предполагайте, что любая операция может выбросить ошибку - и проектируйте интерфейсы с учётом этого
Это не означает, что нужно обрабатывать ошибки в каждом участке кода. Но без привычки предполагать ошибки вы не сможете эффективно с ними работать.
Примеры возможных поломок на стороне клиента браузера:
Любой поток может завершиться с ошибкой.
Любой HTTP‑запрос может вернуть сбой.
Любая DOM‑операция может не найти элемент.
Любой объект может оказаться
null
илиundefined
.Любая функция может выбросить исключение.
Любой компонент может не отрендериться.
С вашим кодом может работать неопытный специалист, который станет причиной ошибок, о которых вы не могли и подозревать
И это только начало
Пример - DOM манипуляция
DOM элемент может быть не найден по разным причинам. Например:
Элемент ещё не отрендерен (ранний доступ)
Непрвильный селектор (если он создается динамически)
Элемент удален из DOM (необязательно вами. Это могут делать и сторонние скрипты, например расширения браузера)
SSR гидрация
// ❌ Ошибка: Cannot read properties of null (reading 'addEventListener')
document.querySelector('#submit-btn').addEventListener('click', handleSubmit);
// ✅ Отказоустойчивый вариант
const btn = document.querySelector('#submit-btn');
if (btn) {
btn.addEventListener('click', handleSubmit);
} else {
console.warn('Кнопка отправки не найдена - событие не назначено');
}
Пример - типизация (Typescript)
JavaScript изначально не типизирован, и TypeScript лишь добавляет статическую типизацию поверх него. Однако типизация не гарантирует, что в рантайме данные будут соответствовать ожидаемому типу - например, с сервера может прийти некорректный или неполный ответ
type IUserFromApi = {
name: string;
age: number;
};
function processUser(user: IUserFromApi) {
// ❌ Даже с типизацией приходящий из api user может быть undefined.
// Операции без валидации аргумента user будут потенциально рискованными
}
function processUser(user: IUserFromApi) {
if(!isUserValid(user)) return; // ✅ Можно, например проверить наличию и валидность или подобрать более подходящее решение под конкретный случай
// ...
}
2. Пользовательский опыт — высший приоритет
Одним из самых главных фундаментов отказоустойчивости является забота о пользователе
Плохой пользовательский опыт - когда пользователь ничего не понимает, а приложение выглядит так, будто «сломалось», хотя тот ничего не делал.
Мы не можем гарантировать, что у пользователя будет стабильный интернет, мощный браузер, последняя версия приложения или же на стороне клиента, что тот получит адекватные данные с сервера. Но мы должны стремиться делать так, чтобы его опыт оставался целостным.
Не всегда получится обеспечить пользователю целостный опыт при сбоях. Но стремление к этому будет учить видеть возможности, вырабатывать подходы и инструменты.
Цель при работе со сбоями - не просто героически закрыть “дыру”, а дать пользователю хороший опыт
Пример - фокусы на устранении ошибки и на пользовательском опыте
Пример фокуса на устранении ошибки:
// Предположим, мы получили ответ с сервера const response = await fetch('/api/product'); const data = await response.json(); const price = data?.price if (price == null) { // ❌ Если нам не пришла цена продукта, то скрываем блок с ценой. // Фокус на устранении "ошибки", но не на пользователе const container = document.querySelector('#price-container'); container?.remove(); // или .style.display = 'none' } else { document.querySelector('#price')!.textContent = `${price} ₽`; }
Пример фокуса на пользовательском опыте:
// Предположим, мы получили ответ с сервера const response = await fetch('/api/product'); const data = await response.json(); const price = data?.price; const priceEl = document.querySelector('#price'); if (price == null) { // ✅ Пользовательский фокус: Не оставляем пользователя в недоумении priceEl.textContent = 'Цена недоступна. Уточните у менеджера.'; } else { priceEl.textContent = `${price} ₽`; }
Пользователь должен получать обратную связь на свои действия - даже при сбое
Если ошибка произошла в ответ на действие пользователя и т.д. - пользователь должен получить понятный и своевременный ответ. Молчание системы или неявные сбои создают ощущение, что «что-то сломалось».
Даже если операция не удалась, важно дать обратную связь (понг в ответ на пинг от пользователя): показать сообщение об ошибке, визуально отметить сбой, предложить повтор действия или объяснить, что делать дальше. Это сохраняет доверие и делает опыт предсказуемым.
Плох тот опыт, когда пользователю приходится раз за разом повторять одно и то же действие, которое ни к чему не ведет
Пользователь, который получает комфортный опыт обязательно к вам вернется (и наоборот)
Пример - загрузка профиля
<button onclick="loadProfile()">Загрузить Профиль</button>
async function loadProfile() {
try {
const res = await fetch('/api/profile');
// ...
} catch (err) {
showErrorNotify('Не удалось загрузить профиль пользователя. Попробуйте позже');
// ✅ В ответ на действие пользователя даже в случае ошибки
// не оставляем его без "Ответа" на совершенное действие
}
}
3. Сбой - один из этапов работы приложения, а не конец
Предотвратить все ошибки невозможно. Но нередко можно дать пользователю возможность восстановиться: пересоздать компонент в нужный момент, повторить запрос, очистить состояние, обновить страницу. Не надо гнаться за тем, чтобы «никогда не падало» - важно, чтобы падение не было концом сессии пользователя.
ВКонтакте, например, при критических сбоях автоматически перезагружает страницу. Это может раздражать, но позволяет избежать зависания сессии (Лучше, чем ничего)
Еще один из подходов, позволяющий восстановить работоспособность при сбоях - graceful degradation
Graceful degradation - это подход, при котором система при сбоях продолжает работать частично, предоставляя пользователю упрощённый, но понятный опыт вместо полной поломки.
Это комплексный подход, который работает особенно эффективно, когда его реализуют совместно и клиентская, и серверная части.
По этой теме подробно и доступно рассказал сотрудник Яндекса Илья Сидоров в докладе и статье на Habr. Настоятельно рекомендую ознакомиться: https://habr.com/ru/companies/yandex/articles/438606/
Пример 1 - подмена "умных" данных
Представим, что у нас система, которая рассчитывает “умные” данные для пользователя (например, рекомендации). В случае сбоя подстраховываемся — показываем дефолтные, упрощённые или последние сохранённые данные.
let route;
try {
route = this.getSmartData(); //
} catch(error) {
route = this.getMinimalData(); // ✅ Восстанавливаем работу системы, даем минимально необходимые данные
}
this.displayRoute(route); // ✅ Система продолжает работать даже в случае ошибки
Пример 2 - чат с поддержкой
Пробуем загрузить чат поддержки. В случае ошибки — выводим номер телефона поддержки, чтобы пользователь, даже в условии сбоя, всё равно мог связаться с поддержкой.
async function loadSupportChat() {
const container = document.getElementById('support-container');
try {async function loadSupportChat() {
const container = document.getElementById('support-container');
try {
const res = await fetch('/api/support/chat-url');
if (!res.ok) throw new Error('Ошибка при загрузке чата');
const { widgetUrl } = await res.json();
const iframe = document.createElement('iframe');
// Пропустим Настройки iframe
container.appendChild(iframe);
} catch (err) {
// ✅ graceful fallback
container.innerHTML = `
<p>Не удалось загрузить чат поддержки.</p>
<p>Вы можете связаться с поддержкой по номеру: <strong>8 800 555-35-35</strong></p>
`;
}
4. Обработка ошибок — это часть архитектуры, а не заплатки
Обработка ошибок - try-catch
, boundary-компоненты
, error reporting, повторные запросы через определенные промежутки времени и другие подобные механизмы, если их использовать с умом, не заплатки и не признаки слабого кода. К ним не нужно относиться как к проявлению неуверенности в своем коде - это базовые инструменты отказоустойчивости, которые помогают локализовать сбои и сохранять стабильность системы.
Заплатка - это спонтанная реакция на баг: срочная вставка try-catch без анализа причины, временное отключение функционала или ловля ошибки «где получилось». А обработка ошибок в архитектуре - это продуманный механизм, встроенный с самого начала.
Сильный код - не тот, который не требует обработки ошибок, а тот, который использует обработки как инструмент
Вам не должно быть стыдно, когда вы используете обработку ошибки. Но стоит задуматься если в вашем коде не зашито ни единой обработки ошибки
Закладывайте все работы с ошибками еще на этапе планирования архитектуры, определяйте критически важный функционал и рискованные места, в которых потенциально могут происходить ошибки
На этапе создания архитектуры можно предусмотреть:
Какие ошибки обрабатывать глобально, а какие - локально в компонентах.
Настроить централизованную обработку ошибок с отправкой данных в сервис мониторинга для быстрого реагирования.
Определить участки с повышенным риском сбоев (внешние API, манипуляции с DOM, сложные вычисления, работу с localStorage и др.)
Выделить критически важный функционал, требующий особой защиты.
Продумать механизмы автоматического восстановления (перезагрузка, повтор запросов, сброс состояния и др.)
(Список далеко не полный)
Если использовать обработку ошибок как заплатки, со временем это приведёт к росту технического долга, усложнению поддержки, увеличению затрат на сопровождение и снижению предсказуемости кода (и, следственно, доверия к коду).
5. Ошибки должны быть изолированы
Хорошая практика не в том, чтобы ошибок не было. Ошибки были, есть и будут и от этого мы никуда никогда не уйдем. Хорошая практика - в изоляции ошибок. Упала одна фича - приложение продолжает работает. Ошибка не должна «протекать» через уровни.
Можно перенести этот принцип в реальный мир. Если у вас в комнате начался пожар, то хорошая пожаростойкая система не даст огню выйти за пределы комнаты на остальную часть дома
В обычном javascript нам это может обеспечить конструкция try-catch
. В React и Angular одним из примером реализации являются boundary-компоненты
Пример - изоляция ошибки
function someClosedMethod(): void {
try {
riskyOperation();
} catch (e) {
// ✅ Обрабатываем ошибку на месте
}
}
// Вызываем внутри метода другой метод
someClosedMethod();
// ✅ Ошибка во вложенном методе не вырывается во внешний
6. Код должен жить дольше своего автора
Это показатель зрелости разработчика и устойчивости всей системы.
Хорошая система не должна зависеть от того, кто с ней работает. Люди приходят и уходят, команда меняется, а код остаётся. Он должен продолжать работать стабильно даже тогда, когда его трогает человек без полного контекста, без глубокого понимания архитектуры и с ограниченным опытом.
Код должен быть написан так, чтобы выдерживать не только нагрузки, но и вмешательства других разработчиков - в том числе тех, кто не знает всей истории, не знаком с архитектурными компромиссами и только осваивается в проекте.
Код должен быть:
Читаемым и предсказуемым: предпочтение простым структурам и явной логике;
Устойчивым к масштабированию: спроектирован так, чтобы его можно было безопасно расширять, не ломая существующую логику.
Основанным на надёжных паттернах: Smart Dumb Components, Explicit Flows, Vertical Slice Architecture и другие
Код, который не рассчитан на то, что в него зайдет менее опытный разработчик с большой вероятностью когда-нибудь сломается
7. Работа с ошибками должна быть направлена на устранение причины, а не на следствия

Обработка ошибки - это не “сделать так, чтобы приложение не падало”, а понять, почему оно могло упасть - и устранить источник.
Просто «не падать» недостаточно. Когда при сбое:
данные молча подменяются заглушками,
исключения подавляются без логов,
или встречается
if (!data) return
без выяснения, почемуdata
нет,
Создается в первую очередь видимость стабильности. Иногда эта видимость действительно совпадает с реальным положением дел - но далеко не всегда, и это самое опасное.
Система продолжает работать, но потенциальные ошибки не будут видны напрямую - они будут просто маскироваться. Такая маскировка усложняет отладку: вместо выявления причины сбоя приходится иметь дело с его отдалёнными последствиями или вовсе оставаться в неведении о возникших проблемах.»
Если вам довелось заняться устранением ошибки - ищите причины, а не устраняйте следствия
Чтобы не просто маскировать сбои и не создавать иллюзию стабильности, важно фиксировать их (Даже если сбой не приводит к падению приложения). Это помогает не упустить реальные проблемы и своевременно реагировать на них.
Если ошибки игнорировать или подавлять без следа, система превращается в «чёрный ящик», где сбои происходят, но остаются незаметными. Такая маскировка осложняет диагностику - приходится бороться с последствиями, а не с причиной.
Обработка ошибки не должна превращать проблемное место в “слепую зону”
Пример - лог в обработке
let data;
try {
data = this.getSomeData();
} catch(error) {
data = this.getErrorData();
console.error(error); // ✅ **Сохраняем сигнал о том, что восстановление системы потребовалось**
}
this.doSomething(data);
8. Не все сбои удаётся обработать правильно
Это нужно обдумать, это нужно понять, это нужно принять
Ошибки можно перехватить - но не всегда понять. Мы можем восстановить интерфейс, вернуть fallback-данные или скрыть сбой от пользователя, но это не всегда гарантирует, что приложение продолжает работать так, как задумано.
Мы не всегда знаем контекст, в котором произошла ошибка, можем неверно интерпретировать её причину или упустить побочные эффекты, которые возникли из-за сбоя и в следствии чего можем неверно выбрать способы обработки или восстановления системы
И чем сложнее функционал, тем труднее предусмотреть все нюансы: обработка может не покрыть весь спектр возможных сценариев, поведение может отличаться от ожидаемого, а проблема - проявиться не сразу.
Раскрою вам тайный-тайный секрет: Даже опытные программисты сеньоры с десятилетним стажем не всегда обрабатывают ошибки верно. Так что и вам стыдиться этого не нужно
Всегда относитесь со скепсисом к тому, как именно вы обрабатываете ошибки
Не думайте: “тут стоит обработка - всё под контролем”. Старайтесь понять, что именно произойдёт после обработки, и действительно ли это то, чего вы хотите.
И обязательно тестируйте, тестируйте, тестируйте сценарий, в котором должна сработать обработка ошибки
9. Баланс отказоустойчивости — не защищай абсолютно всё
Работа над отказоустойчивостью требует усилий: больше кода, ресурсов, внимания к архитектуре, а где-то и повышения квалификации команды. Это не бесплатно - и в плане производительности, и в плане времени команды.
Например:
Повторные запросы (retry) увеличивают нагрузку на сервер и могут привести к задержкам.
Логирование и мониторинг требуют подключения внешних систем, занимающих сетевые ресурсы и увеличивающих время отклика.
Показ пользователю fallback-интерфейсов или уведомлений требует дополнительных дизайнов и тестирования.
Глубокое покрытие проекта отказоустойчивостью требует времени (и, следственно, денег, если говорить о компаниях)
Мы живём в мире, где, как правило, в первую очередь учитываются потребности бизнеса, а не идеальная инженерия. И если ресурсы ограничены, приходится расставлять приоритеты: не всё можно (и нужно) спасать от любого сбоя.
И с юнит-тестами, и с отказоустойчивостью работает один и тот же принцип: покрытие 100% - не цель само по себе. В реальной практике лучший подход - защищать и тестировать самое важное и уязвимое, а не всё подряд.
Слишком «жесткая» отказоустойчивость - может привести к чрезмерной сложности, снижению производительности и ухудшению UX из-за частых сообщений об ошибках.
Слишком «мягкая» - пользователи могут столкнуться с поломками и потерей данных
Обеспечьте надёжность там, где это действительно нужно, но не превращайте отказоустойчивость в самоцель
10. Даже если тебе кажется, что ошибке взяться негде - критичный функционал должен быть защищён
Ошибки и сбои часто происходят не там, где мы их ожидаем, а там, где их вовсе не предусматривали. Немалая часть ошибок - это не явные баги, а упущенные сценарии, неучтённые состояния и внешние факторы.
Поэтому полагаться только на интуицию и «надежный код» - рискованно. Особенно когда речь идёт о критически важном функционале, сбой в котором недопустим.
И даже если вы считаете себя опытным разработчиком и уверены, что не допустите сбоя - нет никакой гарантии, что тот, кто будет работать с вашим кодом после вас, будет так же внимателен.
Важно защищать не только «подозрительные» участки, но и те, где ошибка кажется невозможной - потому что именно они чаще всего и ломаются неожиданно.
Заключение
Отказоустойчивость - не про «безошибочность», а про зрелость системы и заботе о пользователе. Это способность принимать сбои как часть реальности и превращать их в управляемые сценарии, а не катастрофы.
Сила приложения проявляется не в том, что ошибок нет, а в том, как система реагирует на них. Надёжный интерфейс, понятные fallback-механизмы, изоляция сбоев и быстрая возможность восстановления - вот что делает систему действительно устойчивой и доверенной.
Отказоустойчивость - это не просто исправление багов, а продуманное проектирование с учётом потенциальных ошибок, которое позволяет создавать стабильный и предсказуемый опыт для пользователей, даже когда что-то идёт не так.
Если вы не будете уделять отказоустойчивости должного внимания, то рискуете найти себя одним прекрасным будничным вечером, сидя у монитора с пылающей «задницей», заливая на прод один хотфикс за другим.