Программирование RustОбработка ошибокРазработчик серверных приложений на Rust

При проектировании функции валидации как вернуть вызывающему коду все найденные ошибки, а не только первую?

При проектировании функции валидации как вернуть вызывающему коду все найденные ошибки, а не только первую?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нужно представить совокупность ошибок отдельным типом, например структурой с коллекцией диагностик, и вернуть её через Result<УспешноеЗначение, ОшибкиВалидации>. Функция должна пройти все независимые проверки, накопить нарушения, а затем вернуть Ok, если список пуст, или Err со всеми найденными ошибками.

Исторический контекст

Модель Result<T, E> хорошо подходит для отказа с одной причиной: операция либо завершается успешно, либо возвращает одно значение ошибки. Для пользовательской валидации этого часто недостаточно, поскольку исправление только первой ошибки приводит к повторным запросам и ухудшает интерфейс.

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

Постановка проблемы

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

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

Подробное решение

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

Минимальный пример:

#[derive(Debug)] struct ValidationErrors { fields: Vec<(String, String)>, } fn validate(name: &str, age: i32) -> Result<(), ValidationErrors> { let mut errors = Vec::new(); if name.trim().is_empty() { errors.push(("name".into(), "empty".into())); } if !(0..=120).contains(&age) { errors.push(("age".into(), "out_of_range".into())); } if errors.is_empty() { Ok(()) } else { Err(ValidationErrors { fields: errors }) } }

Здесь Result отвечает на вопрос, прошла ли валидация, а ValidationErrors переносит все детали отказа. Порядок проверок должен быть предсказуемым, а коды ошибок — стабильными, если их используют клиенты или тесты.

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

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

Ситуация из практики

HTTP-обработчик регистрации проверяет имя, пароль и адрес электронной почты. Вариант с немедленным возвратом первой ошибки прост и экономен по вычислениям, но заставляет клиента делать несколько циклов исправления. Вариант с Vec<String> собирает всё быстро, однако теряет структуру: клиенту сложно надёжно связать сообщение с полем, а локализация становится хрупкой.

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

Что кандидаты часто упускают

  1. Должен ли тип ошибок валидации реализовывать std::error::Error?

Не обязательно для самого накопления и возврата через Result. Реализация std::error::Error полезна, если ошибка должна участвовать в общем интерфейсе ошибок приложения, поддерживать цепочку источников через source или передаваться в инфраструктурные средства логирования. Для чисто внутренней проверки достаточно собственного типа, но публичный API обычно выигрывает от согласованного интерфейса ошибок.

  1. Что означает пустой список внутри ValidationErrors?

Обычно такой объект не следует считать корректной ошибкой: пустой список означает отсутствие найденных нарушений. Инвариант типа должен запрещать или не создавать ValidationErrors без элементов, а функция должна возвращать Ok, когда коллекция пуста. Это уменьшает число неоднозначных состояний и упрощает обработку на границе приложения.

  1. Следует ли включать в агрегированные ошибки чувствительные исходные значения?

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