Обработка ошибок: result против исключений

Учимся обрабатывать сбои двумя способами OCaml — типобезопасным result для ожидаемых ошибок и исключениями для исключительных.

result — вариантный тип Ok of 'a | Error of 'e, который делает возможный сбой частью возвращаемого значения и типа функции.

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

Тип result

type ('a, 'e) result = Ok of 'a | Error of 'e

let parse_age s =
  match int_of_string_opt s with
  | None -> Error "не число"
  | Some n when n < 0 -> Error "возраст отрицательный"
  | Some n -> Ok n

let show = function
  | Ok n -> Printf.sprintf "возраст: %d" n
  | Error msg -> "ошибка: " ^ msg

Тип parse_agestring -> (int, string) result. Преимущество перед исключениями: ошибка видна в типе и не может быть незаметно «потеряна».

Исключения

exception Empty_list

let head = function
  | [] -> raise Empty_list
  | x :: _ -> x

let safe_head lst =
  try Some (head lst)
  with Empty_list -> None

Стандартная библиотека сама бросает исключения: List.hd []Failure, выход за границы — Invalid_argument, деление на ноль — Division_by_zero.

Когда что выбирать

СитуацияИнструмент
ожидаемый сбой (плохой ввод)result или option
нарушение инвариантаисключение
быстрое прерывание рекурсииисключение (эффективно)
публичный API с явными ошибкамиresult

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

Как работает под капотом

Исключения в OCaml реализованы очень эффективно: raise разворачивает стек до ближайшего try почти без накладных расходов, поэтому их можно использовать даже для управления потоком. Тип result не требует разворачивания стека: это обычное значение, передаваемое вверх по цепочке. Цена за явность — нужно «протаскивать» result через каждый уровень, для чего есть Result.bind и Result.map. Это та же монадическая идея, что у option, роднящая OCaml с Haskell, F# и Scala.

Частые ошибки

  • Использовать исключения для рутинных ошибок. Тогда контракт функции скрыт; лучше result.
  • Ловить все исключения подряд (try ... with _ -> ...). Это маскирует баги.
  • Игнорировать Error при разборе result. Не сводите всё к Ok с заглушкой.

Итоги

  • result (Ok/Error) делает ожидаемый сбой частью типа функции — ошибку нельзя незаметно потерять.
  • Исключения (exception/raise/try ... with) эффективны для исключительных ситуаций.
  • Идиома: result для предсказуемых ошибок, исключения — для нарушений инвариантов.
Проверьте себя
1. Чем result отличается от option?
AНичем
Bresult в случае неудачи несёт информацию об ошибке (Error e), а не просто None
Cresult работает только с числами
Dresult — это исключение
2. Когда уместнее использовать result, а не исключение?
AДля нарушений инвариантов
BДля ожидаемых, предсказуемых сбоев, где важна явность в типе
CДля прерывания глубокой рекурсии
DНикогда
3. Почему исключения в OCaml можно использовать даже для управления потоком?
AОни медленные, но точные
Braise эффективно разворачивает стек до ближайшего try почти без накладных расходов
CОни не разворачивают стек
DОни компилируются в result