---
title: "Ошибки и коды выхода: SilenceUsage"
description: "Cobra обработка ошибок: RunE возвращает error, SilenceUsage и SilenceErrors, коды выхода для скриптов и CI, поток stderr и оборачивание ошибок через %w. С примерами."
course: "Cobra: CLI-приложения на Go"
url: "https://golangmaster.ru/courses/cobra/lessons/14-oshibki-exit-codes/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Ошибки и коды выхода: SilenceUsage

*Курс «Cobra: CLI-приложения на Go», урок 14 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cobra/lessons/14-oshibki-exit-codes/*

Cobra обработку ошибок сводит к короткому контракту: RunE возвращает error, Execute отдаёт его в main, и от того, что main делает дальше, зависит код выхода процесса. Разберём SilenceUsage и SilenceErrors, словарь кодов выхода для скриптов и CI, разделение stdout и stderr и оборачивание ошибок через %w.

Пайплайн зелёный, а бэкапа нет — классический сюжет с CLI-утилитой, которая упала, но вернула процессу ноль. Ноль для CI означает «всё хорошо»: едут следующие шаги, ошибка всплывает через неделю, когда восстановление из несуществующего бэкапа срочно нужно продакшену. В Cobra ответственность за код выхода лежит на двух концах. Первый — RunE возвращает error вместо того, чтобы вызывать os.Exit прямо из команды. Второй — main обрабатывает то, что вернул Execute, и превращает ошибку в ненулевой код. Пропустите второй конец — и любая ошибка превратится в вежливый ноль, потому что Execute ничего не завершает сама. Разберём всю линию целиком: от ошибки в RunE через печать в stderr до кода выхода, который читает шелл, и по пути уберём из вывода шум в виде usage-страницы, которая по умолчанию вываливается после каждой ошибки.

### Cobra обработка ошибок: RunE вместо os.Exit

Контракт простой: RunE возвращает error, Cobra печатает его и usage-страницу, Execute отдаёт ошибку вызывающему. Звать os.Exit внутри RunE не нужно и вредно: os.Exit завершает процесс немедленно, минуя defer, а значит, открытые в команде файлы не закроются и PostRun-хуки из предыдущего урока не выполнятся. Пока ошибка возвращается по цепочке, у программы остаётся шанс прибраться. Стандартный хвост main выглядит так: if err := rootCmd.Execute(); err != nil { os.Exit(1) } — сообщение об ошибке уже напечатано Cobra, main лишь выставляет код. Забытый хвост — самый дешёвый баг года: Execute вернул ошибку, main дошёл до конца нормально, процесс закрылся с нулём, и мониторинг остался в неведении. Проверьте свой main прямо сейчас, это одна строка, — и переходите к тонкостям печати.

### main, который не прячет падение

```go
func main() {
        if err := rootCmd.Execute(); err != nil {
                os.Exit(1)
        }
}
```

Execute печатает Error: с текстом ошибки в stderr и возвращает ошибку; main превращает её в код выхода 1. Этого достаточно для скриптов и CI: ненулевой код — сигнал «упало». Более тонкие коды и собственная печать добавляются поверх этой схемы, а не вместо неё.

### Проверка: код выхода у забывчивого main

**Вопрос:** main выглядит так: rootCmd.Execute() без обработки результата. RunE вернул ошибку. Какой код выхода у процесса?

- 1: Cobra сама вызывает os.Exit при ошибке
- ✅ 0: ошибка напечатана, но код выхода не выставлен
- 2: специальный код ошибки Cobra
- Процесс зависает в ожидании подтверждения

Execute ничего не завершает — она только печатает и возвращает error. Игнорирование возврата оставляет main с нормальным завершением и кодом 0. Именно так зелёный пайплайн пропускает упавшую утилиту, и именно поэтому обработка возврата Execute — не опция.

### SilenceUsage и SilenceErrors: тише — лучше

По умолчанию ошибка в RunE тянет за собой usage-страницу: пользователь опечатался во флаге и получил два экрана списка команд, среди которых ещё нужно найти саму ошибку. Для человека это шум, для скрипта — мусор в stderr. rootCmd.SilenceUsage = true убирает печать usage при ошибках; справка по --help продолжает работать, потому что SilenceUsage глушит только реакцию на ошибку, а не сам механизм usage. SilenceErrors идёт дальше: Cobra не печатает и само сообщение Error:, ошибка возвращается нетронутой, и печать берёт на себя main — один формат, одно место. Флаги можно ставить и на конкретную команду, а проверка в ExecuteC смотрит оба уровня: достаточно SilenceUsage или SilenceErrors на команде или на корне, чтобы соответствующая печать заглохла. Рабочий дефолт для утилит с командами — SilenceUsage на корне: пользователи знают, что флаги перечислены в --help, и не нуждаются в их повторе после каждой опечатки.

### SilenceUsage на корне

```go
var rootCmd = &cobra.Command{
        Use:          "notes",
        SilenceUsage: true,
}

func main() {
        if err := rootCmd.Execute(); err != nil {
                os.Exit(1) // Error: уже напечатан Cobra в stderr
        }
}
```

SilenceUsage глушит печать usage при ошибке: пользователь видит одну строку Error: с текстом вместо страницы команд. Само сообщение Cobra печатает сама, поэтому main только выставляет код выхода. Хотите свой формат ошибки — добавьте SilenceErrors и печатайте в main сами, но выберите одно место печати, а не оба.

### Что напечатает Execute

```go
var rootCmd = &cobra.Command{
        Use:           "notes",
        SilenceUsage:  true,
        SilenceErrors: true,
}
// RunE возвращает errors.New("boom")

_ = rootCmd.Execute()
```

**Что выведет программа?**

- ✅ stderr пуст: ни Error:, ни usage
- Error: boom без usage
- usage без Error:
- Error: boom и usage целиком

SilenceErrors отключил печать сообщения, SilenceUsage — usage-страницы. Cobra молча вернула ошибку вызывающему: печать и код выхода теперь целиком на main. Удобно для своих форматов и опасно для забывчивых: без печати в main ошибка исчезнет бесследно.

### Коды выхода: язык для скриптов и CI

Код выхода — единственный канал, через который утилита говорит с обёртками: шелл читает $?, CI сравнивает его с нулём, оркестратор решает, перезапускать ли контейнер. База простая: ноль — успех, всё остальное — ошибка. Но у разветвлённых утилит полезен словарь кодов: 1 — общий провал, 2 — ошибка использования, как поступает ряд системных утилит, 3 — частичный успех вроде «синхронизировано 9 из 12». Словарь фиксируется в документации рядом с описанием команд, а не в голове автора, — иначе скриптописатели начнут угадывать. Реализация аккуратно укладывается в тип: RunE возвращает свой тип ошибки с полем Code, main разбирает его через errors.As и зовёт os.Exit с нужным числом. Дополнение к словарю: код 128+N шелл показывает, когда процесс убит сигналом N — знакомый всем 130 после Ctrl+C это 128+2. Возвращайте такой код и сами, когда прерывание пришло извне, а cleanup успел завершиться: скрипты отличат отмену от обычного провала. И помните свойство os.Exit из начала урока: он завершает процесс немедленно и defer не выполняет, поэтому весь словарь живёт в main, а не в командах.

### Тип ошибки с кодом выхода

```go
type ExitError struct {
        Code int
        Err  error
}

func (e *ExitError) Error() string { return e.Err.Error() }
func (e *ExitError) Unwrap() error { return e.Err }

func main() {
        err := rootCmd.Execute()
        if err == nil {
                return
        }
        var ee *ExitError
        if errors.As(err, &ee) {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(ee.Code)
        }
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
}
```

ExitError несёт код поверх обычной ошибки и разворачивается через Unwrap, поэтому errors.Is и errors.As продолжают видеть исходную причину. RunE возвращает &ExitError{Code: 3, Err: ...} там, где сценарий «частичный успех», а main один раз разбирает тип и выставляет код. Печать в обеих ветках идёт в os.Stderr: stdout остаётся чистым для перенаправлений — правило разделения потоков работает и здесь. Словарь кодов живёт комментарием рядом с этим типом.

### Проверка: os.Exit и defer

**Вопрос:** RunE открыл файл и зарегистрировал закрытие через defer f.Close(). Внутри RunE вызван os.Exit(3). Что произойдёт?

- Файл закроется defer-ом, затем процесс завершится
- ✅ Процесс завершится с кодом 3, файл не закроется
- Процесс завершится с кодом 1: os.Exit в RunE запрещён
- Ошибка вернётся в Execute как обычно

os.Exit завершает процесс немедленно и defer не выполняет — это свойство функции из package os, а не особенность Cobra. Поэтому os.Exit живёт только в main, после того как все defer-ы отработали по естественному пути выполнения.

### Оборачивание ошибок: %w и контекст

Ошибка «permission denied» без контекста — детектив для пользователя: что именно не открылось и в какой команде? fmt.Errorf("read %s: %w", path, err) добавляет путь и причину в одну строку, а глагол %w сохраняет исходную ошибку внутри: errors.Is(err, os.ErrNotExist) и errors.As продолжают работать по всей обёрнутой цепочке. В CLI это превращается в конкретику двух уровней: RunE добавляет операцию — что делали, main добавляет команду — где делали. Разверните привычку из языка Go: каждый слой добавляет контекст, ничего не теряя, и ваши сообщения станут самодиагностируемыми — по одной строке из лога видно, какая команда, на каком файле и по какой причине упала. Полная механика %w, errors.Is и errors.As разобрана отдельным уроком в продвинутом курсе, здесь мы используем только её следствия.

### Обёртка и разбор в main

```go
func runAdd(cmd *cobra.Command, args []string) error {
        data, err := os.ReadFile(storagePath)
        if err != nil {
                return fmt.Errorf("read storage: %w", err)
        }
        // ... работа с заметками
        return nil
}

func main() {
        if err := rootCmd.Execute(); err != nil {
                if errors.Is(err, os.ErrNotExist) {
                        fmt.Fprintln(os.Stderr, "хранилище не найдено, выполните notes init")
                        os.Exit(2)
                }
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1)
        }
}
```

RunE оборачивает ошибку чтения через %w, сохраняя os.ErrNotExist внутри цепочки. main разворачивает её через errors.Is и переводит в подсказку с отдельным кодом 2: скрипт по коду отличит «нет хранилища» от общего провала и запустит init. Это словарь кодов в действии — без единого os.Exit внутри команды.

> **Ошибка, напечатанная дважды.** Включили SilenceErrors на корне, а в main забыли печать — пользователь видит только код выхода, без единого слова о причине. Обратная комбинация хуже: SilenceErrors не поставили, а в main напечатали err повторно — каждая ошибка выходит дважды: сначала Error: от Cobra, потом ваша копия. Выберите одну из двух схем и не смешивайте: либо молчаливая Cobra с печатью в main, либо болтливая Cobra с bare os.Exit(1). И в обеих схемах пишите в stderr через fmt.Fprintln(os.Stderr, err) — оставляя stdout чистым для результата, который скрипты перенаправляют в файлы.

### stderr против stdout: кто что читает

Разделение потоков — половина культуры вывода CLI. В stdout пишется результат: список заметок, идентификатор созданной записи, путь к файлу — то, ради чего команду зовут и что скрипты ловят через перенаправление. В stderr — всё остальное: ошибки, предупреждения, прогресс, диагностику. Форматирование здесь то же, что и у fmt.Println, только поток другой: fmt.Fprintln(os.Stderr, ...) и fmt.Fprintf(cmd.ErrOrStderr(), ...). Cobra по умолчанию вежлива: и Error:, и usage при ошибке уходят в stderr, если вывод не переопределён через SetOut — проверьте это, прежде чем парсить вывод своих команд в шелле. Нарушение разделения стоит дорого: notes list 2>/dev/null | grep id перестанет находить идентификаторы, как только одна строка диагностики уедет в stdout, а отладка таких утечек — то ещё удовольствие, потому что отдельно взятый запуск в терминале выглядит правильно.

### Куда что печатаем

| Поток | Что печатаем | Кто читает |
| --- | --- | --- |
| stdout | результат: список заметок, id созданной записи | пользователь и скрипты с перенаправлением |
| stderr | ошибки, предупреждения, прогресс | человек перед экраном и логи CI |
| код выхода | итог: 0 — успех, ненулевой — провал | шелл, CI, оркестратор |

### Соберите тихий main

Впишите поле тишины usage, поток для ошибок и вызов завершения.

```go
rootCmd.Silence___ = true

func main() {
        if err := rootCmd.Execute(); err != nil {
                fmt.Fprintln(os.___, err)
                os.___(1)
        }
}
```

Подсказка: SilenceUsage глушит usage при ошибке; сообщения об ошибках идут в os.Stderr; завершение с кодом — os.Exit(1).

Правильные ответы в пропусках: `Usage`, `Stderr`, `Exit`

### Соберите обработку ошибки из шести строк

Расставьте: тишина usage, выполнение, проверка ошибки, печать, код выхода, закрытие if.

Правильный порядок строк:

1. `rootCmd.SilenceUsage = true`
2. `err := rootCmd.Execute()`
3. `if err != nil {`
4. `  fmt.Fprintln(os.Stderr, err)`
5. `  os.Exit(1)`
6. `}`

### Карточки: ошибки и коды выхода

- **SilenceUsage** — Глушит печать usage-страницы при ошибке; --help продолжает работать. Ставьте на корень: скриптам и людям дамп флагов после опечатки не нужен.
- **SilenceErrors** — Глушит печать Error: — ошибка возвращается нетронутой, и печатает её main. Выберите одно место печати, чтобы не дублировать сообщения.
- **Код выхода** — Ноль — успех, ненулевой — провал. Словарь кодов (2 — ошибка использования, 3 — частичный успех) фиксируйте в документации; реализация — ExitError и errors.As в main.
- **os.Exit** — Немедленное завершение без defer. Только в main после Execute: os.Exit в RunE оставит файлы открытыми и пропустит PostRun-хуки.
- **%w** — Глагол fmt.Errorf, оборачивающий ошибку с сохранением исходной: errors.Is и errors.As видят причину сквозь обёртки, а сообщение копит контекст.
- **stderr против stdout** — Результат — в stdout, ошибки и прогресс — в stderr через fmt.Fprintln(os.Stderr, ...). Cobra и Error:, и usage при ошибке пишет в stderr.

### Что дальше

Схема ошибки — RunE, SilenceUsage, код выхода — теперь финальная, осталось научиться её проверять. В уроке про [тестирование команд](/courses/cobra/lessons/20-testirovanie-cobra/) Execute вызывается из теста, вывод ловится буфером вместо терминала, и ошибки сравниваются без os.Exit — ровно поэтому os.Exit и держат в main, подальше от RunE. Как всё это собирается в цельную утилиту, покажет [итоговый проект](/courses/cobra/lessons/25-itogovyj-proekt/): команды add/list/done с хранилищем, кодами выхода и тестами. Механика оборачивания разобрана отдельно в уроке про [ошибки и %w](/courses/go-advanced/lessons/15-oshibki-wrapping/) продвинутого курса, обзор остальных тем — на странице [Cobra](/courses/cobra/). Как всегда с CLI: код выхода видит только живой процесс — соберите утилиту локально, уроните её специально и проверьте значение $? в шелле.

## Коротко: что запомнить

- RunE возвращает error, main превращает его в код выхода: забытый os.Exit(1) после Execute — нулевой код у упавшей утилиты.
- SilenceUsage убирает usage-дамп при ошибке, SilenceErrors — печать Error:; печатайте ошибку в одном месте.
- Код выхода — канал для скриптов и CI: ноль — успех, словарь кодов — для различимых провалов.
- os.Exit завершает процесс без defer: только в main, никогда в RunE.
- Оборачивайте ошибки через fmt.Errorf с %w: контекст растёт, errors.Is и errors.As продолжают работать.
- Результат — в stdout, ошибки — в stderr через fmt.Fprintln(os.Stderr, ...).
- Две схемы печати: молчаливая Cobra плюс main или болтливая Cobra плюс bare os.Exit(1) — не смешивайте.

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

Проверьте main своей утилиты: обрабатывает ли она ошибку из Execute. Затем включите SilenceUsage на корне, добавьте тип ExitError с полем Code и верните код 3 из команды sync, когда часть заметок не синхронизировалась. Уроните команду специально: неверный флаг, отсутствие хранилища, частичный успех — и выпишите полученные коды выхода через $? в терминале. Отдельно проверьте прерывание: запустите долгую команду и прибейте её Ctrl+C — echo $? покажет 130, то есть 128+2, даже без единой строки вашего кода об ошибках.

## Вопросы и ответы по уроку

### Почему Cobra печатает usage при каждой ошибке?

Это поведение по умолчанию: ошибка тянет за собой usage-страницу. Задайте SilenceUsage: true на корневой команде — останется одна строка Error:. Флаг на конкретной команде гасит usage только для неё, на корне — для всего дерева.

### Как вернуть свой код выхода из команды Cobra?

Не вызывайте os.Exit в RunE. Верните обычную ошибку или тип-обёртку с полем Code, а в main после Execute разберите её через errors.As и вызовите os.Exit с нужным числом. Дефолт для любой ошибки — os.Exit(1).

### Куда Cobra печатает ошибку — в stdout или stderr?

В stderr: и сообщение Error:, и usage при ошибке идут в поток ошибок, если вывод не переопределён SetOut. Ваш вывод через fmt.Println уходит в stdout, поэтому ошибки не попадут в перенаправленный результат; для своих сообщений используйте fmt.Fprintln(os.Stderr, ...).
