Почти в любом приложении или платформе нужно думать о двух типах сценариев: человеческом и программном.

Есть и дополнительные разделения: вы общаетесь с членами своей команды или с разработчиками из других команд или компаний? Это особенно важно при создании API или сервисов, где апстрим-разработчики будут перехватывать и обрабатывать ваши ошибки.

Первый шаг — адресовать сообщение правильному человеку в правильной ситуации. Мы все видели ошибки, явно не предназначенные для нас, например когда сайт показывает стек-трейс конечному пользователю.

Чтобы определить аудиторию, начните с выбора категории ошибки. Для наших целей пяти категорий из таблицы 3-1 достаточно для покрытия большинства случаев.

Ценность диагностик

Создание хорошо структурированных диагностик с полезными сообщениями — это невероятно ценное и высокоэффективное вложение времени. Для многих приложений и платформ со сложными и открытыми входными данными диагностики являются основным интерфейсом: большую часть времени пользователь проводит, разбираясь с ошибками и переходя к следующей.

Заполнение электронных форм — это постоянное получение сообщений о пропущенных или некорректных данных. По меньшей мере половина моего времени программирования уходит на работу с ошибками и правилами линтинга. Даже работа в текстовом редакторе превратилась в непрерывный процесс взаимодействия с подчёркнутым текстом и предложениями перепроверить или переформулировать.

И всё же при проектировании ПО ошибки часто остаются вне поля зрения: их нет на скриншотах, в маркетинговых материалах или списках методов API, поэтому о них легко забыть.

Автономные агенты особенно ярко подсвечивают эту проблему. Они регулярно получают сообщения об ошибках, возникших из-за их действий, и получают инструкции исправить ошибки на их основе. Если сообщение недостаточно полезно — агент проваливает задачу. Перебор вариантов — медленный и дорогой процесс. А так как агенты тарифицируются по использованию, стоимость этого напрямую измерима.

Совет: диагностики могут быть самым важным интерфейсом вашего продукта.

Сценарии для диагностик

При работе с ошибками, предупреждениями и связанными с ними сообщениями важно учитывать широкий спектр сценариев: от выявления edge cases до понимания того, как разработчики будут автоматизировать реакции, и как конечные пользователи будут интерпретировать и исправлять ошибки.

Чем лучше вы понимаете знания ваших пользователей, формируете user stories и моделируете пользовательские взаимодействия, тем лучше будут ваши диагностики.

Для пользователей мы даём контекстные и действенные ошибки.

Для разработчиков мы тщательно подбираем типы ошибок, коды и метаданные, чтобы получатели могли корректно восстановиться после ошибки.

В этой главе вы научитесь создавать действительно полезные предупреждения и ошибки. Мы рассмотрим, как:

  • Понять сценарий — персону, для которой предназначена ошибка, и её контекст

  • Дать пользователю достаточно информации для понимания проблемы

  • Делать сообщения об ошибках действенными, подсказывающими, что делать дальше

  • Осознанно выбирать коды и типы ошибок, чтобы апстрим-разработчики могли корректно обслуживать своих пользователей

  • Поднимать ошибки на уровне API или UI, чтобы сообщения писались с полным контекстом намерений пользователя

  • Сдвигать влево (shift left), то есть выявлять ошибки как можно раньше, до наступления серьёзных последствий

Категоризация сценариев ошибок

При написании ошибок нужно сделать несколько ключевых выборов.

С точки зрения пользователя:

  • Какое сообщение об ошибке показать?

С точки зрения разработчика:

  • Какой класс или код у ошибки?

  • Какие метаданные нужны для точной диагностики?

Таким образом, почти в любом приложении или платформе нужно думать о двух типах сценариев: человеческом и программном.

Есть и дополнительные разделения: вы общаетесь с членами своей команды или с разработчиками из других команд или компаний? Это особенно важно при создании API или сервисов, где апстрим-разработчики будут перехватывать и обрабатывать ваши ошибки.

Первый шаг — адресовать сообщение правильному человеку в правильной ситуации. Мы все видели ошибки, явно не предназначенные для нас, например когда сайт показывает стек-трейс конечному пользователю.

Чтобы определить аудиторию, начните с выбора категории ошибки. Для наших целей пяти категорий из таблицы 3-1 достаточно для покрытия большинства случаев.

Таблица 3-1. Категории ошибок
Таблица 3-1. Категории ошибок

Начните с мысленной классификации каждой ошибки. Это даёт сильную подсказку о том, с кем вы разговариваете — со своей командой, с другими разработчиками или с пользователями — и когда ошибка может быть исправлена: во время выполнения или на этапе разработки. Это поможет выбрать правильный словарь и предложить полезные действия (см. таблицу 3-2).

Таблица 3-2. Когда и для кого предназначены разные категории ошибок
Таблица 3-2. Когда и для кого предназначены разные категории ошибок

Эти пять сценариев требуют радикально разных стратегий. Например, если assertion срабатывает в продакшене, это обычно катастрофа. Код оказался в состоянии, не предусмотренном авторами, что приводит к непредсказуемому поведению — чаще всего к кр��шу или плохому сообщению об ошибке. Иногда последствия хуже, например повреждение данных.

В некоторых языках assertions вообще удаляются в продакшене ради производительности, поэтому на них нельзя полагаться в критичных местах. В любом случае конечный пользователь не должен успешно взаимодействовать с такими ошибками.

Для некоторых приложений конечные пользователи — не однородная группа. В этом случае сообщения должны учитывать персону. Классический пример — ошибка Preconditions Not Met из-за отсутствия прав доступа. Пользователь — администратор или обычный юзер? От этого зависит, даём ли мы прямые инструкции или предлагаем обратиться к администратору.

Знание персон помогает говорить на языке онтологии пользователя. (Онтология была определена в главе 2 как структурированный граф известных понятий.)

Пример «PC Load Letter» пытался попросить пользователя загрузить бумагу в принтер, но провалился, потому что говорил с неправильной персоной. «PC» означало paper cassette, а «Letter» — формат бумаги 8.5×11. Возможно, правильнее было бы назвать лотки A, B и C и написать: «Загрузите бумагу в лоток B».

Категоризация ошибок на практике

Рассмотрим пример.

К какой из пяти категорий относится деление на ноль (в Python — ZeroDivisionError)?

Представим метод, который вычисляет среднее значение метрики за период.
Представим метод, который вычисляет среднее значение метрики за период.

Если метод выбросит ZeroDivisionError, когда массив метрик пуст, вызывающий код будет в замешательстве. Чтобы понять ошибку, придётся знать внутреннюю реализацию функции.

Совет: пользователи и разработчики не должны понимать реализацию, чтобы понять ошибку.

Если ваш код — не калькулятор, деление на ноль — это Assertion, которую нужно находить на этапе тестирования. Её следует избегать, делая предварительную валидацию.

Но к какой категории относится сама валидация? Причины len(metrics) == 0 могут относиться к разным сценариям (см. таблицу 3-3).

Таблица 3-3. Категории ошибок деления на ноль
Таблица 3-3. Категории ошибок деления на ноль

Для каждого случая потребуются разные сообщения и действия, а значит — разные проверки и правильный момент их выполнения, когда доступен нужный контекст.

Сообщения об ошибках и предупреждениях

Создание диагностических сообщений объединяет системное и пользовательское мышление. Вы точно знаете, что произошло, но должны перейти на перспективу пользователя и объяснить ситуацию в понятных ему терминах. Иначе получаются сообщения вроде LaTeX’овского «underfull hbox (badness 10000)».

Пользователь хочет знать две вещи:

  • Что именно произошло, в терминах онтологии продукта, и каков эффект ошибки

  • Что с этим можно сделать

Кейс: Channelz

Channelz — это вымышленная компания-разработчик SaaS, создающая инструмент для коммуникации на рабочем месте, похожий на Slack, Microsoft Teams или Discord.
Элиза работает в команде API, а её коллега Дэнг — технический лидер.

В Channelz можно писать личные сообщения коллегам или отправлять сообщения в «каналы» — это группы сотрудников, объединённых вокруг определённой темы; например, у команды API может быть канал #team-api-eng. Хэндл Элизы — @elisek, а Дэнга — @deng.

Channelz разрабатывает API, который позволяет отправлять сообщения от ботов — как напрямую пользователям, так и в каналы. Клиенты хотят использовать его для отправки различных уведомлений.

Перед тем как писать код, Элиза набрасывает быстрый дизайн интерфейса для разработчиков и показывает его Дэнгу. Сообщения Channelz могут отправляться либо конкретным людям, либо в канал — чтобы оповестить сотрудников о проблеме или о завершении задачи.

Метод в Python SDK, который они будут поставлять клиентам, выглядит так:

Elise проектирует интерфейс SDK и показывает Deng. Метод Python SDK выглядит так
Elise проектирует интерфейс SDK и показывает Deng. Метод Python SDK выглядит так
Она также набрасывает несколько сценариев использования для Дэнга.
Она также набрасывает несколько сценариев использования для Дэнга.

Дэнг смотрит на дизайн Элизы и просит её перечислить сценарии сбоев. Элиза показала только успешные варианты использования API — когда вызывающая сторона уже знает, что делать. Но что происходит до этого? Если рассматривать сессию кодинга пользователя как путешествие, Элиза показала только конечную точку. Это всё равно что попросить маршрут в онлайн-картах, а в ответ получить только пин с пунктом назначения.

Элиза придумывает несколько сценариев. Она поднимает один важный сценарий, на котором мы и сосредоточимся: что если пользователь или канал, переданный в API, невалиден?

Делайте сообщения об ошибках и предупреждениях полезными и побуждающими к действию

Во многих случаях знать, что произошло, — это лишь половина дела. Пользователям часто нужно подсказать, что делать дальше. А для операций чтения — и всё чаще с использованием ИИ — вы даже можете исправить ошибку за них, как в функции Google «Показаны результаты для: [исправление]», или как это делают ассистенты по коду и тексту, автоматически исправляя ошибки

Все мы потратили бесчисленные часы, разбираясь с сообщениями об ошибках и пытаясь понять, что делать, иногда выясняя после долгих расследований, что решение было элементарным

В этом разделе вы увидите, как системно улучшать диагностику. Для этого нужно сопереживать своей аудитории, начиная с категоризации сценариев, которую мы делали ранее.

Вернёмся к примеру Channelz. Допустим, вы вызвали API так:

bot.send_message(message="The sky is falling!", channel="@barnyard-friends")

и получили ошибку

Cannot deliver a Channelz message to channel '@barnyard-friends': channel does not exist.

Понятно ли, что пошло не так? Возможно, потребуется время, и если вы не очень знакомы с терминологией Channelz, вы можете не понять, что каналы начинаются с #, а не с @.

Гораздо лучше предложить пользователю несколько вариантов действий:

Cannot deliver a Channelz message to channel '@barnyard-friends': it is prefixed with @.
Did you mean to pass it into 'users'? Or did you mean '#barnyard-friends'?

Channelz мог бы даже проверить аккаунты, чтобы узнать, существует ли #barnyard-friends (и доступен ли он пользователю), и показать:

Channel @barnyard-friends is prefixed with @, but we found a channel #barnyard-friends.
Is that what you meant?

Сокращая «пространство поиска» для пользователя и предлагая конкретные действия, вы можете сэкономить ему часы времени или вовсе предотвратить отказ от использования продукта. Даже простое «попробуйте ещё раз через минуту» после системной ошибки может повысить процент успешных завершений сценариев.

Иногда советы бывают сложными. Может потребоваться объяснить пользователю новые концепции — например, что такое каналы — или провести его через серию решений. В таких случаях стоит давать ссылки на документацию, посвящённую устранению именно этой ошибки. Например:

See https://channelz.io/docs/errors/invalid_channel to learn how to resolve this error.

Поднимайте ошибки на уровне интерфейса

Если бы писать хорошие сообщения об ошибках было просто, инженеры делали бы это чаще. Одна из главных причин, почему этого не происходит, — для этого часто требуется продуманная организация кода.

Чтобы написать лучшее сообщени�� об ошибке, нужны два вида информации:

  1. Что произошло в системе?

  2. Что пользователь пытался сделать?

Проблема в том, что в реальной жизни эти данные находятся в разных частях кода.

Одна часть — ближе к API или UI — знает, кто пользователь и чего он хотел. Именно она лучше всего подходит для объяснения пользователю, что делать дальше, не вдаваясь в детали реализации.

Другая часть — глубоко внутри логики обработки — знает, где именно произошла ошибка.

Это мы уже видели при категоризации ошибок. Когда мы делим на ноль, операция деления в Python прекрасно знает, какой закон математики был нарушен, но не имеет ни малейшего представления о контексте использования — а значит, не может выдать полезное сообщение.

Лучшее место для генерации ошибок — на границе между системой и пользователем, где можно объединить знания «снизу вверх» и «сверху вниз». (Альтернатива — протаскивать весь пользовательский контекст вниз по стеку; я рассмотрю это позже.

Обычно это делают двумя способами:

  1. Проактивно выбрасывают ошибки, выполняя проверки на границе API.

  2. Перехватывают ошибки нижнего уровня и переупаковывают их в более подходящую форму.

Рассмотрим оба подхода на примере Channelz.

Проверки заранее (Upfront Validations)

Команда Observability в ChickenLittle сталкивается с другой проблемой. Функция, отвечающая за on-call уведомления, называется alert_team.

get_team_metadata читает базу данных дежурств, которую сотрудники ChickenLittle настраивают через интерфейс по адресу

https://corp.chickenlittle.io/oncalls/

Что произойдёт, если team_metadata[oncall_user] окажется невалидным? Напомним, что send_messageвыбрасывает такую ошибку:

Error: Cannot deliver a Channelz message to ‘[@gooseyloosey]’ because ‘@gooseyloosey’ has been deactivated.

Сообщение точное, но когда Гузи Лузи уволился из компании, пользователи не понимали, как это исправить. Поэтому команда Observability добавила проверку заранее, чтобы сразу подсказать, что делать:

Когда пользователь отсутствует, метод сообщает:

Cannot send a Channelz alert to team ‘barnyard-friends’.
On-call user ‘@gooseyloosey’ does not exist.
Update your oncall rotation at https://corp.chickenlittle.io/oncalls/barnyard-friends.

Этот более высокоуровневый API alert_team гораздо лучше отражает намерение вызывающей стороны, чем send_message, и это отличная база для полезных ошибок.

Совет: выбрасывайте ошибки на самом внешнем уровне API или приложения, где вы лучше всего понимаете пользовательский сценарий.

Этот подход сработал, но он не идеален. Можете заметить проблемы?

Рассмотрим альтернативу — переупаковку ошибок, которые выбрасывают зависимости.

Переупаковка ошибок

У upfront-валидаций было две проблемы:

  1. Они дорогие — требуют дополнительного запроса к API Channelz.

  2. Хуже того, они дублируют проверку граничных случаев, которая уже есть внутри Channelz (например, проверку деактивации аккаунта).

А эта информация всё ещё полезна: например, сотрудник мог просто сменить хэндл из-за смены имени — и это уже другой сценарий исправления.

Команда Observability жалуется Элизе. Они предпочли бы писать код вроде этого:

Обратите внимание на from error в последней строке — это механизм «сцепленных исключений» в Python, который сохраняет исходную ошибку. Во многих языках есть аналогичные механизмы.

Результат будет таким:

ChannelzUserNotFoundError: User @looseygoosey’s account has been deactivated.

The above exception was the direct cause of the following exception:

ValueError: Cannot send a Channelz alert to team ‘barnyard-friends’.
On-call employee ‘@looseygoosey’ does not exist.
Update your oncall rotation at https://corp.chickenlittle.io/oncalls/barnyard-friends.

Сообщение длиннее, но в нём есть всё, что может понадобиться пользователю. Observability просит Элизу добавить более конкретные исключения в SDK Channelz, чтобы они могли так делать.

С учётом того, сколько сотрудников уходит из ChickenLittle, Элиза надеется, что «небо там всё-таки не падает». После этого они с Дэнгом начинают думать о том, как сделать исключения более программируемыми.

Диагностируйте как можно раньше (Shift Left)

Ранняя диагностика часто называется shift left и даёт множество преимуществ как системе, так и пользователям.

Совет: сдвигайтесь влево — давайте пользователям диагностику как можно раньше.

Для системы это снижает потребление ресурсов, обрывая бесполезные пути выполнения. Это критично, например, для защиты от DoS-атак. Также это защищает код от обработки непредсказуемых входных данных и предотвращает баги вроде потери данных.

Пользователи выигрывают ещё больше. Ранние ошибки экономят время — вспомните, сколько вы выигрываете, получая ошибку в IDE, а не в продакшене. Кроме того, пользователи лучше помнят, что именно они сделали, когда получают быстрый фидбэк.

Четыре распространённых техники shift left:

  1. Статические проверки

  2. Проверки заранее

  3. Возможность тестировать

  4. Запрос подтверждений у пользователя

Первые две применяются часто, а вот последние две — к сожалению, редко.

Статические проверки

Статические проверки можно выполнять дёшево, без глубокого анализа или сетевых вызовов, в отличие от динамических.

Совет: отделяйте дешёвые проверки от дорогих и выполняйте дешёвые как можно раньше и чаще.


Это касается не только типов и линтеров, но и пользовательских продуктов.

Например, в формах можно сразу проверять длину почтового индекса, контрольные суммы кредитных карт, UPC или ISBN. Если контрольная сумма не сходится — мы точно знаем, что номер просто неверный.

Проверяйте заранее

Валидации помогают не только писать хорошие ошибки, но и сдвигать диагностику влево.

Представьте, что вы постирали одежду, а потом узнали, что сушилка занята на несколько часов. Табличка на входе сэкономила бы вам массу проблем.

То же самое с переводами денег — важно проверить счёт назначения до списания средств.

Многошаговые процессы почти всегда выигрывают от upfront-валидаций.

Дайте пользователям возможность тестировать

Один из самых недооценённых сценариев — тестирование. Клиенты проходят долгий путь, прежде чем успешно использовать продакшен.

Если вы делаете платформу для разработчиков — создайте фейк. Это высокодостоверная версия продакшена, использующая тот же код, но без нестабильных зависимостей вроде баз данных и внешних сервисов.

Для Channelz такой фейк должен:

  • работать локально или в памяти;

  • позволять создавать пользователей и каналы;

  • максимально повторять поведение продакшена.

Пример из реального мира — Stripe с его test mode, где можно симулировать ошибки, не двигая реальные деньги.

Запрашивайте подтверждения у пользователя

Подтверждения — это «лежачие полицейские» в интерфейсе. Они громче предупреждений, но мягче ошибок.

Они объясняют, почему ввод может быть опасным или неожиданным, и дают шанс исправить его или подтвердить.

Пример — Google «Did you mean…»

Один из приёмов — dry run с последующим подтверждением.

В Channelz, например, можно симулировать retry-политику и предупредить:

Your retry_policy (currently initial_interval_seconds=1, backoff_coeffi cient=1.0, max_attempts=Infinite) will result in 601 attempts in 600 seconds. This exceeds our limit of 50. You may ignore this by adding .ignore(Errors::RetryPolicySpamminess) to your policy.

Да, эвристика может быть неточной, но она помогает пользователям раньше понять проблему.

Итоги

Думая о диагностике, начинайте с опыта пользователя:

  • что он знает,

  • что ему нужно знать,

  • что ему делать дальше.

Категоризируйте ошибки:

SystemInvalid User ArgumentPreconditionInvalid Developer ArgumentAssertion.

В сообщениях:

  • давайте контекст;

  • используйте понятия продукта;

  • предлагайте действия и альтернативы.

Понимайте ошибки на уровне интерфейса и ищите способы shift left: подтверждения, статические проверки, фейки.

Домашнее задание

  1. Найдите в интернете видео «Windows Blue Screen of Death Evolution».

    • Для кого эта ошибка предназначена?

    • Проанализируйте экран, совпало ли выбор вашей аудитории и оошибка?

  2. Представьте, что вы работаете над e-commerce приложением с API submit_order:

    • Какую категорию ошибок имеют невалидные user ID и cart ID?

    • Какую категорию — невалидный CVV?

    • А пустой CVV?

    • Напишите полезное сообщение об ошибке для отсутствующего CVV.

    • Корзины удаляются через 24 часа. Какая это категория ошибки?

    • Как можно сдвинуть эту ошибку влево, чтобы пользователь не собирал корзину заново?