Сообщения об ошибках и логи: как читать чужие и писать свои
03:40. Алерт. В канале инцидента висит строка:
2026-07-16T09:14:22Z ERROR checkout failed to charge card: post https://api.pay/v1/charges: dial tcp 10.0.3.7:443: connect: connection refused
Инженер, который её читает, за следующие тридцать секунд должен ответить на четыре вопроса: что мы пытались сделать, где сломалось, кто отказал и есть ли смысл ретраить. Все четыре ответа в этой строке есть. Ни один не выражен по-английски так, как учат в языковой школе: здесь нет подлежащего, нет артиклей, нет ни одного глагола в личной форме. connection refused — это не «соединение отказалось». Это ярлык, приклеенный к коду ECONNREFUSED где-то в семидесятых, и означает он совершенно конкретную вещь: удалённый хост получил наш SYN и ответил RST, то есть машина жива, а слушателя на порту нет.
Эта глава про два навыка, которые выглядят как один. Читать чужие сообщения и логи так, чтобы извлекать максимум за минимум времени. И писать свои так, чтобы через полгода незнакомый человек в другом часовом поясе понял их с первого раза.
Оговорка про уровень, которую мы дали в обзоре трека: материал предполагает, что вы уже читаете технический текст со словарём. Мы не объясняем, что такое причастие; мы объясняем, почему connection refused — причастие и что из этого следует для отладки.
Почему это отдельный диалект
Английский сообщений об ошибках — не «упрощённый» и не «плохой» английский. Это устоявшийся регистр со своей грамматикой, ближайший бытовой аналог которого — газетные заголовки и телеграммы. Служебные слова выброшены, потому что занимают место и не несут информации.
| Регистр | Текст | Где встречается |
|---|---|---|
| Разговорный | The server we were trying to reach did not accept our connection. | письмо коллеге |
| Документация | The connection was refused by the remote host. | текст в доках |
| Телеграфный | connection refused |
строка ошибки |
Все три верны. Третий — единственный, который вы увидите в логе, и единственный, который невозможно правильно прочесть, не зная конвенций. Что именно в нём выброшено:
- Артикли.
no such file or directory, а не there is no such file. Артикль возвращается, когда без него возникает двусмысленность:the connection was closed by the remote host— здесьtheуказывает на конкретное, уже упомянутое соединение. - Подлежащее.
failed to open config— кто failed? Подразумевается «я, программа». То же опущение, что в русском «не удалось открыть». - Личная форма глагола.
connection refused— этоconnection [was] refused: причастие прошедшего времени без связки. Отсюда главная ловушка, о которой ниже. - Пунктуация как синтаксис. Двоеточие в
A: B: C— не знак препинания, а оператор вложенности: «A, а причина в том, что B, а причина в том, что C».
Причина, по которой регистр таков, прозаическая: строки ошибок пишутся внутри кода, там, где у автора нет ни места, ни контекста. Автор ECONNREFUSED не знал, что через полвека его два слова будут читать миллионы людей, для большинства которых английский — второй язык. Список системных строк из man 3 errno — вероятно, самый читаемый корпус английских «предложений» в истории вычислительной техники, и он же задал стиль всему остальному.
Анатомия строки: где заканчивается логгер и начинается человек
Любая строка лога состоит из двух текстов с разными авторами и разной грамматикой.
Префикс формирует логгер. Его словарь конечен и учится один раз: уровни, имена компонентов, trace_id, caller. Читать его надо как таблицу, а не как текст.
Сообщение — цепочка обёрток, собранная снизу вверх: ядро сформулировало connection refused, сетевой слой обернул это в dial tcp …, HTTP-клиент — в post …, наш код — в failed to charge card. Отсюда правило чтения, экономящее больше всего времени:
Начало цепочки говорит, что вы хотели. Конец цепочки говорит, что помешало. Диагноз всегда в конце.
Русскоязычный читатель по привычке начинает с начала и цепляется за failed to charge card — за самое понятное, но наименее информативное звено. Приучите себя читать такие строки с хвоста.
В Go цепочка строится явно, через %w, и конвенция формулировок вытекает прямо из механики склейки:
func chargeCard(ctx context.Context, id string) error {
if err := c.post(ctx, "/v1/charges", id); err != nil {
// Плохо: глагол отказа в каждом звене даёт в логе
// "failed to charge card: failed to post: failed to dial: connection refused"
// return fmt.Errorf("failed to charge card: %w", err)
// Хорошо: звено — именная группа. Слово "failed" произносит один раз тот, кто печатает лог:
// "charge card cus_42: post https://api.pay/v1/charges: connection refused"
return fmt.Errorf("charge card %s: %w", id, err)
}
return nil
}
Это не стилистическая придирка, а следствие грамматики: цепочка через двоеточие читается как один синтаксический период, и повтор глагола в каждом звене его разрушает. Официальная формулировка — в Go Code Review Comments: строки ошибок не начинаются с заглавной буквы и не заканчиваются знаком препинания, именно потому что их почти всегда вставляют внутрь большего текста. Механику %w, errors.Is и errors.As разбирает блог Go про ошибки 1.13; то же правило и по той же причине действует в Rust — см. C-GOOD-ERR.
В Java и .NET конвенция противоположная: сообщение исключения — полноценное предложение с заглавной буквы и точкой, потому что печатается отдельной строкой, а не вклеивается в чужую. Если пишете на обоих языках, держите различие в голове: одинаковая формулировка будет ошибкой стиля в одном из них.
Глаголы отказа: карта территории
Английский различает виды отказа тоньше, чем русское «не получилось». Каждое из этих слов — гипотеза о причине, и подмена одного другим стоит часов отладки.
Сетевые отказы
Четыре строки, которые вы будете видеть чаще всего, означают четыре разные вещи:
| Строка | Errno | Что произошло физически | Ретраить? |
|---|---|---|---|
connection refused |
ECONNREFUSED |
Хост жив, ответил RST: на порту никто не слушает | Да, но сначала проверьте, запущен ли сервис |
connection timed out |
ETIMEDOUT |
Ответа не было вовсе: пакеты уходят в никуда | Да, с backoff; вероятен файрвол или перегрузка |
connection reset by peer |
ECONNRESET |
Соединение было установлено и оборвано на ходу | Осторожно: запрос мог быть выполнен |
no route to host |
EHOSTUNREACH |
Маршрутизация не знает, куда слать | Нет, ретрай не поможет: чините сеть |
Обратите внимание на by peer в третьей строке. Это редкий случай, когда английская строка ошибки называет агента, и именно поэтому она информативнее остальных. peer здесь — не «коллега», а «противоположная сторона соединения». Практический вывод: reset и timed out означают, что вы не знаете, выполнился запрос или нет; что с этим делать — в главе про идемпотентность и гарантии доставки.
Отказы доступа
| Строка | Смысл | Типичная ошибка чтения |
|---|---|---|
permission denied (EACCES) |
Не хватает прав на объект: файл, каталог, сокет | Путают с «нет такого файла» — а это разные ветки кода |
operation not permitted (EPERM) |
Операция запрещена в принципе, нужен root или capability | Кажется синонимом предыдущего, но приходит из других мест |
401 Unauthorized |
На деле «не аутентифицирован»: мы не знаем, кто вы | Слово тянет к «нет прав» — признанная ошибка именования |
403 Forbidden |
Мы знаем, кто вы, и вам нельзя | Путают с 401 в обе стороны |
401 — идеальный учебный пример: имя статуса выбрано неудачно, и документация прямо это признаёт — семантика статуса «unauthenticated». Когда стандарт вынужден объяснять собственное название, доверяйте определению, а не слову. Механизмы за этими статусами — в аутентификации и авторизации.
Отказы данных и состояния
invalid / malformed / unrecognized / unsupported — четыре диагноза, которые в русском схлопываются в «некорректный». malformed JSON — распарсить не удалось вообще, сломан синтаксис. invalid email address — распарсили, но нарушено смысловое правило. unrecognized field "usr_id" — синтаксис в порядке, но такого поля мы не знаем. unsupported media type — знаем, что это, но не умеем обрабатывать. А разница между unsupported, not yet supported и no longer supported — это разница между «никогда», «будет позже» и «было и убрали».
Родственная тройка про жизненный цикл API, которую путают постоянно: deprecated — объявлено устаревшим, но работает, это предупреждение, а не отказ (русское «устарело» звучит как «сломалось», отсюда паника на ровном месте); obsolete — поддержки нет, но код может лежать; removed — вызов упадёт.
Слова про состояние — метафоры, и буквальный образ помогает удержать точный смысл: stale — «чёрствый», данные не испорчены, просто старые (stale read); dangling — «болтающийся», ссылка есть, объекта нет; orphaned — «осиротевший», объект жив, а владелец умер; leaked — ресурс выделен и не освобождён. И три слова про нестабильность, которые в русском все становятся «плавающим»: flaky (тест падает случайно), intermittent (проявляется время от времени), transient (кратковременный, проходит сам). Только transient error означает «ретрай уместен».
Пассив и пропавший агент
Главный источник непонимания — не словарь, а синтаксис: английская строка почти всегда прячет того, кто совершил действие. Permission denied — кем? The request was rejected — кем? В русском переводе агент исчезает так же, и читатель по инерции достраивает «мне отказали» — и идёт проверять свои права. А отказать мог прокси, а не целевой сервис.
Приём, который стоит довести до автоматизма: прочитав строку, спросите себя «кто?» и попробуйте назвать агента вслух.
| Строка | Скрытый агент | Что это меняет |
|---|---|---|
connection refused |
Ядро на удалённой машине | Идти надо на удалённый хост, а не в свой код |
permission denied |
Ядро локальной машины, проверка на inode | Проверять права файла, а не токен |
403 Forbidden |
Сервис или стоящий перед ним прокси | Сначала проверьте, дошёл ли запрос до сервиса |
certificate verify failed |
Ваш TLS-клиент | Проблема на вашей стороне: не тот store доверия |
Отсюда прямое правило для сообщений, которые пишете вы: если знаете агента — назовите его. payments-api rejected the request информативнее, чем request rejected, и стоит трёх лишних слов. Тот же совет даёт руководство по стилю сообщений PostgreSQL — один из лучших документов на эту тему вообще: активный залог, конкретный субъект, никаких bad, illegal и unknown вместо точных слов.
Процедура: как разобрать незнакомую ошибку
Это не «понимание английского», а алгоритм. Он работает, даже если половина слов вам незнакома.
время, уровень, компонент"] P --> C{"Есть цепочка
через двоеточия?"} C -->|да| LAST["Взять последнее звено —
это диагноз"] C -->|нет| LAST LAST --> V["Найти глагол или причастие,
спросить «кто?»,
восстановить агента"] V --> INV["Выделить инвариант:
убрать пути, id, числа, адреса"] INV --> SRC{"Зависимость
есть локально?"} SRC -->|да| GREP["rg -F по исходникам:
код вокруг строки — лучшая документация"] SRC -->|нет| WEB["Поиск по инварианту в кавычках
+ имя библиотеки"] GREP --> H["Гипотеза о причине"] WEB --> H H --> T{"Подтвердилась?"} T -->|нет| BACK["Взять предыдущее звено цепочки"] T -->|да| DONE["Чинить"] BACK --> V
Ключевой шаг — предпоследний, и он не про язык. Строка ошибки — уникальный ключ к исходному коду. Она почти всегда существует в репозитории зависимости ровно в одном месте, и код вокруг неё отвечает на вопрос «при каких условиях это печатается» точнее любой документации:
# Инвариантная часть — без путей и чисел; -F отключает regex
rg -F 'connection refused' "$(go env GOMODCACHE)"
grep -rn 'invalid literal for int' "$(python -c 'import sys; print(sys.prefix)')/lib"
Выделение инварианта — навык, который экономит часы. В строке
failed to load config from /srv/app/releases/20260716T091422Z/config.yaml: yaml: line 42: did not find expected key
инвариант — это did not find expected key. Всё остальное — ваши данные, которых нет ни у кого в интернете. Люди, которые ищут строку целиком, ничего не находят и делают вывод «уникальная проблема».
Чтение стектрейсов: направление имеет значение
Python прямо пишет направление чтения в первой строке, и её почти все игнорируют:
Traceback (most recent call last):
File "app/handlers.py", line 42, in create_order
total = int(request.form["amount"])
ValueError: invalid literal for int() with base 10: ''
most recent call last — «самый недавний вызов последним», то есть читать снизу: внизу место, где сломалось, вверху — как мы туда попали. Само сообщение непрозрачно даже для тех, кто знает слова: literal — текстовая запись значения, with base 10 — разбор шёл в десятичной системе (у int() есть параметр base). Итог: пустая строка не является записью числа. Никакой мистики, но фразу надо один раз развернуть.
Java устроена наоборот:
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:790)
... 12 more
Caused by: org.postgresql.util.PSQLException: FATAL: password authentication failed for user "app"
Caused by: — «вызвано тем, что», и цепочка идёт вниз. Читать надо последний Caused by: верхнее исключение почти всегда бесполезная обёртка фреймворка. ... 12 more — «ещё 12 таких же», JVM свернула повторяющийся хвост стека. И обратите внимание на FATAL: внутри сообщения PostgreSQL: это его собственный уровень серьёзности, попавший внутрь чужого текста. Уровни вкладываются друг в друга, и ERROR снаружи не обязан совпадать с FATAL внутри.
Уровни как шкала
Слова уровней — шкала с определённым порядком, стандартизованная в RFC 5424: emerg, alert, crit, err, warning, notice, info, debug. warning — «что-то не так, но работа продолжается»; если после него процесс падает, слово выбрано неверно. fatal и panic — «процесс сейчас завершится»; fatal в английском значит не «ужасный», а «смертельный для этого процесса» — Go log.Fatal буквально вызывает os.Exit(1), слово описывает механику, а не эмоцию. Типичная ошибка — писать ERROR там, где ничего не сломалось: дежурный привыкает к красному цвету и перестаёт его видеть, и арифметику этого привыкания разбирает глава про алерты.
Логи: где именно в них живёт английский
Строка структурированного лога — не предложение, а запись. Английский в ней сосредоточен в двух местах: в поле msg и в именах полей.
{"ts":"2026-07-16T09:14:22Z","level":"error","msg":"charge card","service":"checkout",
"customer_id":"cus_42","attempt":3,"timeout_ms":2000,"error":"connection refused","trace_id":"a1b2c3"}
Вид глагола: -ing против -ed
Английское причастие несёт то же различие, что русский совершенный и несовершенный вид, — и у русскоязычных инженеров здесь есть готовая интуиция.
| Формулировка | Значение | Когда писать |
|---|---|---|
starting migration |
процесс начался и идёт | перед началом длинной операции |
migration completed |
завершилась успешно | после успеха |
migration failed |
завершилась отказом | после отказа |
retrying request |
повторяем прямо сейчас | перед повтором |
connecting / connected |
«подключаемся» / «подключились» | пара строк вокруг установления соединения |
Правило: -ing — операция идёт, причастие прошедшего времени — операция закончилась. Хуже всего смешивать: если половина кодовой базы пишет started migration, а половина starting migration, по логу невозможно понять, зафиксирован факт запуска или факт завершения. Русское «Запуск миграции» так же двусмысленно — тот случай, когда английский строже родного языка.
Порядок слов в именах полей
Английское составное существительное строится так, что главное слово стоит последним, а всё перед ним — определения. retry count — это count, а retry уточняет какое.
| Правильно | Неправильно | Почему |
|---|---|---|
retry_count |
count_retry |
главное слово — count, оно последнее |
request_id |
id_request |
то же самое |
duration_ms |
ms_duration |
единица измерения — суффикс, а не префикс |
user_email |
email_user |
«email пользователя», а не «пользователь почты» |
id_request — калька с русского «идентификатор запроса», где главное слово идёт первым; носитель прочитает это как «запрос идентификатора», то есть наоборот. Конвенции имён метрик формализованы в Prometheus naming practices (базовые единицы, суффикс _total для счётчиков), имена атрибутов — в семантических соглашениях OpenTelemetry (http.request.method, server.address). Брать эти словари готовыми выгоднее, чем изобретать свои: половина споров об именовании исчезает вместе с необходимостью их вести. Про саму телеметрию — глава про наблюдаемость.
msg — константа, значения — в полях
import logging
log = logging.getLogger("checkout")
# Плохо: английский собирается на лету и ломается на числах,
# а агрегатор видит миллион уникальных сообщений вместо одного
log.error(f"Deleted {n} items from cart {cart_id}") # "Deleted 1 items" — носитель споткнётся
# Хорошо: msg — неизменяемая именная группа, всё переменное вынесено в поля
log.error("delete cart items", extra={"deleted": n, "cart_id": cart_id})
Причин две, и они складываются. Инженерная: постоянный msg — ключ группировки, интерполяция делает каждую строку уникальной и ломает агрегацию. Языковая: английское согласование числа не выживает в интерполяции (1 items, 1 files were deleted), а костыли вроде item(s) выглядят как признак того, что автору было всё равно. Вынести число в поле — единственное честное решение: deleted=1 не имеет грамматического числа и потому не может с ним не согласоваться. Третья причина всплывает при локализации: собранная из кусков фраза непереводима, потому что в других языках другой порядок слов.
И то, чего в логах быть не должно: пароли, токены, заголовки Authorization, номера карт, персональные данные. Лог уходит в хранилище с другими правами доступа, живёт месяцами и попадает в скриншоты в чатах; failed to authenticate user with password hunter2 — не языковая ошибка, а инцидент безопасности (см. управление секретами).
Как писать сообщение, которое поймут
Формула, к которой сходятся почти все руководства по стилю:
Что мы делали + что ожидали + что получили на самом деле + что теперь делать.
Не обязательно все четыре части в одном сообщении, но если нет ни одной — сообщение бесполезно.
Правый верхний угол устроен одинаково: точно названный объект плюс очевидное следующее действие. port 8080 already in use не содержит инструкции, но действие выводится однозначно — и это нормально.
Одно сообщение — один факт. Failed to load config or connect to database означает, что автор сам не знает, что случилось.
Показывайте значения, а не характеристики значений. expected an integer, got "abc" вместо invalid value. Пара expected … got … — самая ценная конструкция жанра: она задаёт направление сравнения. Первое — то, чего хотел код, второе — то, что пришло на самом деле; перепутать их местами при чтении вывода тестов — классическая ошибка.
Активный залог и названный субъект. payments-api returned 503 вместо a 503 was returned.
Никаких please, oops и восклицательных знаков. В сообщении об ошибке please не считывается как вежливость — оно считывается как шум между читателем и информацией; так же считают руководство Google и Microsoft Writing Style Guide. Отдельно: русская привычка ставить запятую после «пожалуйста» переносится в Please, try again — в английском запятой там нет, и она сразу выдаёт неносителя.
Не обвиняйте читателя и не извиняйтесь вместо объяснения. You entered an invalid date → date must be in YYYY-MM-DD format, got 16/07/2026. Разница не в вежливости: вторая версия содержит спецификацию формата, первая — нет. А Sorry, something went wrong — это отказ сообщать информацию, оформленный как вежливость.
Одно слово на одну вещь. Если в документации сущность называется workspace, ошибка не имеет права говорить project: читатель потратит время, выясняя, одно это или разное. Та же дисциплина терминологии, что в документировании требований.
Стабильный код + изменяемый текст. Формулировку вы будете улучшать; те, кто грепает ваши логи, — нет. Дайте им error_code, и текст можно переписывать свободно. Для HTTP API это оформлено стандартом RFC 9457 Problem Details: type — стабильный идентификатор, title — короткая человеческая формулировка, detail — конкретика случая. Обратите внимание, как стандарт распределяет регистры: title телеграфный и постоянный, detail — полное предложение. Та же двухуровневая схема, что в PostgreSQL, где primary message пишется строчными без точки, а detail и hint — законченными предложениями.
Планка, на которую стоит смотреть: компилятор Elm сделал качество сообщений отдельной инженерной целью (Compiler Errors for Humans), Rust пошёл тем же путём, и сегодня error[E0382]: borrow of moved value с подчёркнутым фрагментом и блоком help: — фактический стандарт индустрии. Разберите структуру их вывода: заголовок телеграфный, объяснение обычным английским, подсказка императивом. Три регистра в одном сообщении, каждый на своём месте.
Мастерская: как это читает носитель
Реальные формулировки из кода русскоязычных команд. Для каждой: как её прочтёт носитель, почему так вышло и как переписать.
1. Oops! Something went wrong. Please try again later.
Читается дружелюбно и уклончиво, как сообщение маркетплейса; в инженерном контексте — «мы не знаем, что случилось, и не собираемся выяснять».
Переписать: failed to create order: payments-api timed out after 5s (request_id=a1b2c3).
2. Please, check the correctness of entered data.
Читается как машинный перевод. Три сигнала сразу: запятая после Please (в английском её нет), тяжёлая номинализация the correctness of entered data вместо глагола, отсутствие детерминатива перед entered data.
Переписать: email must contain @, got "user.example.com".
3. Impossible to connect to database.
Читается как обрывок: конструкция требует формального подлежащего (it is impossible to…), без него фраза повисает. Плюс impossible — «невозможно в принципе», а не «сейчас не вышло».
Переписать: cannot connect to database: dial tcp 10.0.3.7:5432: connection refused.
4. Not enough of memory.
Читается как лишний предлог: enough управляет существительным напрямую. Ошибка родом из русского родительного падежа: «недостаточно памяти» → «of memory».
Переписать: out of memory: tried to allocate 2 GiB, limit is 1 GiB.
5. The operation was finished with error.
Читается как пассив с оттенком «кто-то извне её завершил», плюс пропущенный артикль (with an error). Носитель для этого смысла использует один глагол.
Переписать: import failed after 1200 of 5000 rows.
6. In case of error contact to administrator.
Читается как калька: contact в английском переходный, предлог to после него — ошибка («обратиться к»). Плюс сама рекомендация бесполезна: какому администратору, как.
Переписать: contact your workspace owner or open a ticket in #platform-support.
7. Wrong login or password.
Читается дважды мимо: login — это действие входа, а не имя пользователя (username), а wrong в этом контексте звучит по-детски, стандартное слово incorrect.
Переписать: Incorrect username or password. — и намеренно не уточнять, что именно неверно: расплывчатость здесь не небрежность, а защита от перебора учётных записей (user enumeration).
8. Error: error occurred while processing of request.
Читается как текст, написанный чтобы что-нибудь стояло в этом месте: слово error дважды, номинализация вместо глагола, лишний of. И Data is absent из соседней строки туда же: absent в английском говорят про людей на собрании, про данные — missing.
Переписать: process order 4711: decode payload: unexpected EOF.
9. Timeout
Читается как существительное без контекста: чей таймаут, какой лимит, сколько ждали, что делали — ничего.
Переписать: read timeout after 30s waiting for payments-api /v1/charges.
10. User not found в логе аутентификации
Читается нормально — но одновременно бесполезно (какой пользователь, в какой системе) и опасно: подтверждает существование учётной записи тому, кто читает логи.
Переписать: в лог — authenticate: no user for subject=sub_9f2 in realm=corp; наружу — общая формулировка из пункта 7.
Ложные друзья, которые дорого стоят
Не список слов, а места, где неверное понимание меняет решение.
actual— «фактический», не «актуальный». Вexpected 3, actual 5речь о том, что пришло на самом деле; русское «актуальное значение» — этоcurrent value.eventually— «рано или поздно», причём гарантированно, а не «возможно».eventually consistent— не «может быть, будет согласовано», а «согласуется через некоторое время»; понимание этого слова как «возможно» ведёт к неверным выводам о гарантиях системы.resolve— в контексте DNS «разрешить имя в адрес», а не «решить проблему».failed to resolve host— не смогли получить IP.validatevsverify— первое про соответствие правилам формы (валидируем email по шаблону), второе про соответствие истине (верифицируем подпись).assert— «объявить инвариант».assertion failed— не «утверждение провалилось», а «инвариант нарушен».graceful— «с соблюдением процедуры».graceful shutdown— не «изящное», а «с доработкой текущих запросов».fails silently— «падает, ничего не сообщая». Это диагноз, а не наречие образа действия.
Более широкий разбор межъязыковой интерференции — в главе про типичные ошибки.
Один отказ — два текста
Одно событие требует двух разных сообщений, и самая частая ошибка — написать один текст и показать его обоим адресатам.
телеграфно, агент скрыт L->>S: dial tcp 10.0.3.7:443: connect: connection refused Note over L,S: добавлен адрес, но не смысл S->>Log: charge card cus_42: post /v1/charges: connection refused Note over S,Log: добавлены намерение и идентификатор Log->>E: алерт + trace_id E->>S: гипотеза: payments-api не поднят S->>U: We could not process your payment.
Your card was not charged. Try again in a few minutes. Note over S,U: другой регистр, ноль технических деталей
Инженерное и пользовательское сообщения расходятся по трём осям: словарь (технический против бытового), длина (телеграмма против предложения) и содержание (диагноз против последствий и действия). Пользователю важно ровно одно, чего нет в техническом тексте: деньги не списаны. Инженеру важно ровно одно, чего нет в пользовательском: какой хост отказал. Как писать пользовательскую половину — предмет главы про текст в интерфейсе; здесь важен сам факт разделения: если ваш err.Error() попадает пользователю на экран, у вас нет ни одного из двух текстов.
Жизненный цикл в логах
Схема делает видимыми три вещи, которые чаще всего забывают. Различие transient и permanent должно попадать в текст: дежурному не нужно гадать, ретраил ли код — card declined окончательно, retrying in 2s нет. Номер попытки — поле, а не часть фразы: attempt=2 агрегируется, second attempt нет. И «сдались» — отдельное состояние с собственным сообщением, по которому строится алерт.
Английский giving up в логах абсолютно нормален и не звучит неформально. Это к вопросу о том, что телеграфный регистр не равен «формальному»: он допускает give up, bail out, fall back, back off, drop, но не допускает please и oops.
Что смотреть на ревью
Короткий список для чтения чужого дифа. Формулировки самих замечаний — тема главы про комментарии на ревью; здесь только предмет.
- Сообщение называет конкретный объект — файл, поле, хост, идентификатор?
- Есть ли
expected … got …там, где сравниваются значения? - Не потерян ли исходный
errпри оборачивании (в Go — есть ли%w)? Не повторяется ли глагол отказа в каждом звене? - Регистр и точка соответствуют конвенции языка?
msg— константа, переменное вынесено в поля? - Нет ли в сообщении секретов, токенов, персональных данных?
-ingи-edиспользованы последовательно с остальным кодом?- Пользовательский текст отделён от инженерного? Слово из сообщения совпадает со словом из документации?
Мини-практика на неделю
- Разбор цепочки. Возьмите пять строк ERROR из прода за неделю. Для каждой отрежьте префикс, найдите последнее звено, назовите агента вслух по-русски. Не смогли — кандидат на переписывание.
- Инвариант. Возьмите три ошибки из чужих библиотек, выделите инвариант, найдите его в исходниках через
rg -Fи прочитайте условие возникновения. Почти всегда оно оказывается не тем, что вы предполагали. - Аудит
msg. Грепните кодовую базу на интерполяцию внутри сообщений логов (f",fmt.Sprintfвнутриlog.) и перенесите переменные в поля хотя бы в одном модуле. - Чтение образцов. Спровоцируйте три разные ошибки компилятора Rust или Elm и прочитайте вывод целиком, включая блоки
help:иnote:. Это самый концентрированный корпус хорошо написанного инженерного английского, доступный бесплатно и локально.
Упражнений на «расширение словаря» здесь намеренно нет: слова из сообщений об ошибках запоминаются намертво, когда стоили вам двух часов отладки, и почти не запоминаются из карточек. Как поддерживать язык системно — последняя глава трека.
Итог
- Английский сообщений об ошибках — телеграфный регистр: без артиклей, без подлежащего, с причастиями вместо личных глаголов. Он ближе к газетному заголовку, чем к речи.
- Цепочка через двоеточия читается с конца: начало — намерение, конец — диагноз.
- Агент почти всегда скрыт пассивом. Привычка спрашивать «кто?» после каждой строки отказа экономит часы, потраченные не в том месте.
- Точность словаря отказов — инженерная информация:
refused,timed out,reset,unreachable— четыре разные гипотезы, а не синонимы. - Строка ошибки — уникальный ключ к исходному коду зависимости: выделяйте инвариант, ищите в исходниках, читайте условие.
- Своё сообщение стройте как «что делали + что ожидали + что получили + что делать»: называйте объект и субъект, показывайте значения, не извиняйтесь и не пишите
please. - В логах
msg— константа, переменное — поля;-ingдля процесса, причастие прошедшего времени для результата; главное слово в имени поля — последнее. - Инженерный и пользовательский тексты об одном отказе — два разных текста. Один вместо двух означает, что нет ни одного.
Источники
man 3 errno— канонический словарь системных отказов и их точных значений.- PostgreSQL Error Message Style Guide — лучший короткий документ про грамматику сообщений: регистр, залог, разделение primary/detail/hint.
- Go Code Review Comments: Error Strings, Working with Errors in Go 1.13, Rust API Guidelines: C-GOOD-ERR — конвенции формулировок и оборачивания.
- Google developer documentation style guide: error messages и Microsoft Writing Style Guide: error messages — отраслевые нормы.
- RFC 5424, Syslog Protocol — шкала уровней; RFC 9457, Problem Details for HTTP APIs — структура машинно-читаемой ошибки.
- GNU Coding Standards: Formatting Error Messages — откуда взялся формат
file:line: message. - Elm: Compiler Errors for Humans — как выглядит планка.
- Prometheus naming practices и OpenTelemetry semantic conventions — готовые английские словари для имён метрик и атрибутов.
- Brian Kernighan, Rob Pike. The Practice of Programming — про то, что сообщение об ошибке должно называть программу, вход и причину.
- Nielsen Norman Group: 10 Usability Heuristics — эвристика №9 про распознавание и восстановление после ошибок, для пользовательской половины текста.
Что дальше
Мы разобрали текст, который пишется для машины и читается человеком в спешке. Следующий жанр устроен наоборот: он полностью человеческий, крайне короткий и при этом читается чаще всего остального, что вы пишете, — заголовок коммита живёт в истории проекта десятилетиями и попадает в глаза каждому, кто делает git log или git blame.
Коммиты и pull request: короткий текст, который читают чаще всего