Как map_err преобразует ошибку в Result, не изменяя успешное значение?
map_err применяет переданную функцию только к варианту Err и заменяет исходную ошибку результатом этой функции. Вариант Ok проходит без изменений, поэтому метод меняет тип или представление ошибки, но не обрабатывает успешное значение и не устраняет сам сбой.
В Rust ошибки представлены обычными значениями перечисления Result<T, E>, а не исключениями, которые автоматически распространяются по стеку вызовов. Поэтому языку нужны удобные способы преобразовывать успешные значения и ошибки при последовательной композиции функций.
Комбинатор map_err решает узкую задачу: позволяет адаптировать ошибку одного слоя к типу ошибки другого слоя без ручного сопоставления с match. Это особенно полезно на границах между библиотечным, инфраструктурным и прикладным кодом.
Низкоуровневая функция может возвращать, например, std::io::Error, тогда как публичный API приложения должен возвращать собственный тип ошибки. Если передать исходную ошибку напрямую, абстракция вызывающего слоя будет зависеть от деталей реализации.
Неверный выбор операции тоже опасен. Использование map изменит успешное значение, но оставит тип ошибки прежним, а попытка применить map_err для восстановления после сбоя лишь преобразует описание ошибки и не превращает Err в Ok.
Для Result<T, E> вызов map_err имеет концептуальный результат Result<T, F>, где F — тип, возвращаемый функцией преобразования ошибки. При Ok(value) функция преобразования не вызывается и возвращается Ok(value) с тем же успешным значением.
При Err(error) Rust передаёт ошибку в замыкание или функцию, получает новое значение и возвращает Err(new_error). Таким образом, map_err не выполняет повторную операцию, не делает восстановление и не объединяет вложенные Result; для этих задач применяются другие операции, например or_else или and_then.
Здесь read_to_string возвращает Result<String, io::Error>. При успехе строка сохраняется, а при ошибке io::Error оборачивается в AppError::Config, поэтому итоговый тип функции — Result<String, AppError>.
Преобразование может также добавлять контекст, например имя операции или идентификатор ресурса. При этом важно решить, сохранять ли исходную ошибку внутри нового типа: простое преобразование в строку удобно для вывода, но часто уничтожает структурированную информацию и затрудняет анализ причины сбоя.
map_err не требует реализации std::error::Error сам по себе. Для него достаточно функции, которая принимает исходную ошибку и возвращает новое значение; требования трейтов появляются только там, где их явно устанавливает используемый API.
Сервис загружает конфигурацию из файла. Внутри используется файловая ошибка операционной системы, но наружу нужно возвращать доменную ошибку, чтобы обработчик запроса не зависел от конкретного способа хранения конфигурации.
Можно оставить io::Error как есть. Это проще и сохраняет все исходные данные, но связывает публичный контракт с файловой системой и усложняет последующую замену файла на удалённое хранилище.
Можно преобразовать ошибку в строку. Такой вариант удобен для быстрого сообщения пользователю, однако теряет машинно проверяемый вид причины: вызывающий код уже не сможет надёжно отличить отсутствие файла от отказа в доступе.
Выбранный вариант — доменный тип с вложенной исходной ошибкой, полученный через map_err. Он скрывает инфраструктурную деталь на уровне API, сохраняет диагностику и позволяет позднее добавить другие варианты, например ошибку удалённого хранилища. Результат — стабильный контракт и возможность корректно классифицировать сбои.
Дополнительный вопрос 1: Меняется ли успешное значение при применении map_err?
Нет. При варианте Ok функция преобразования ошибки не вызывается, а успешное значение передаётся дальше без преобразования. Если нужно изменить именно T, применяется map, а если нужно выполнить следующую fallible-операцию — and_then.
Дополнительный вопрос 2: Может ли map_err превратить ошибку в успешный результат?
Нет. Он сохраняет форму Result: Err(error) превращается только в Err(new_error). Для восстановления с переходом к Ok нужен механизм вроде or_else, который получает ошибку и может вернуть как Ok, так и Err.
Дополнительный вопрос 3: Зачем сохранять исходную ошибку внутри нового типа, если уже добавлен контекст?
Вложенная исходная ошибка сохраняет структурированные сведения: код операционной системы, категорию сбоя или возможность проверить источник через цепочку причин. Если заменить её только текстом, сообщение останется пригодным для человека, но программная обработка и диагностика станут менее надёжными.
На практике доменная ошибка часто содержит исходную ошибку как поле или причину, а пользовательское сообщение формируется отдельно. Это позволяет одновременно скрыть внутреннюю реализацию API, сохранить данные для журналирования и не привязывать бизнес-логику к тексту ошибки.