Try и Either: обработка ошибок

В Scala ошибки можно не бросать, а возвращать — как обычное значение, которое нельзя случайно проигнорировать.

«Ошибка как значение — это ошибка, о которой компилятор знает и заставляет вас с ней считаться.»

Option говорит «значение есть или нет», но не объясняет почему его нет. Для этого есть Try и Either.

Try: успех или исключение

Try оборачивает вычисление, которое может бросить исключение, в значение: Success(x) при удаче или Failure(e) с исключением при ошибке.

import scala.util.{Try, Success, Failure}

def divide(a: Int, b: Int): Try[Int] =
  Try(a / b)

divide(10, 2) match
  case Success(v) => println(s"Результат: $v")   // Результат: 5
  case Failure(e) => println(s"Ошибка: ${e.getMessage}")

divide(10, 0) match
  case Success(v) => println(s"Результат: $v")
  case Failure(e) => println(s"Ошибка: ${e.getMessage}")  // деление на ноль
Try[Int]
  /        \
Success(5)   Failure(исключение)
  удача         поймана ошибка
            (не упала программа)

Either: два явных исхода

Either представляет одно из двух значений: Left (по соглашению — ошибка) или Right (успех, «right» = «правильно»). В отличие от Try, тип ошибки выбираете вы сами.

def parseAge(s: String): Either[String, Int] =
  s.toIntOption match
    case Some(n) if n >= 0 => Right(n)
    case Some(_)          => Left("возраст отрицательный")
    case None             => Left("это не число")

println(parseAge("25"))   // Right(25)
println(parseAge("-3"))   // Left(возраст отрицательный)
println(parseAge("xx"))   // Left(это не число)

map и for тоже работают

val result = for
  a <- parseAge("20")
  b <- parseAge("5")
yield a + b
println(result)   // Right(25)

val bad = for
  a <- parseAge("20")
  b <- parseAge("xx")
yield a + b
println(bad)   // Left(это не число)

Та же идея на Python ▶

# Аналог Try/Either на Python
def divide(a, b):
    try:
        return ("ok", a // b)        # как Success
    except ZeroDivisionError as e:
        return ("err", str(e))       # как Failure

tag, value = divide(10, 0)
if tag == "ok":
    print("Результат:", value)
else:
    print("Ошибка:", value)

# Either-стиль: (сторона, значение)
def parse_age(s):
    try:
        n = int(s)
        return ("right", n) if n >= 0 else ("left", "отрицательный")
    except ValueError:
        return ("left", "это не число")
print(parse_age("25"))   # ('right', 25)
print(parse_age("xx"))   # ('left', 'это не число')

Как работает под капотом (JVM)

Try и Either — обычные sealed-иерархии (как Option): Success/Failure и Right/Left — это case-классы. Try(expr) внутри оборачивает выражение в try/catch JVM: если исключение поймано, оно сохраняется в Failure вместо распространения вверх по стеку. Поскольку у обоих типов есть map/flatMap, они работают в for-выражениях, и ошибка «короткозамыкает» цепочку, как None в Option.

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

  • Путать стороны Either. По соглашению ошибка — Left, успех — Right; map работает по правой стороне.
  • Использовать Try для ожидаемой бизнес-логики. Для предсказуемых исходов лучше Either с понятным типом ошибки.
  • Игнорировать Failure. Обработайте обе ветки в match, иначе теряете информацию об ошибке.

Best practices

  • Используйте Try для кода, который может бросать исключения (ввод-вывод, парсинг чужих API).
  • Используйте Either для ожидаемых ошибок бизнес-логики с информативным типом слева.
  • Соединяйте операции в for-выражениях — ошибка прервёт цепочку автоматически.

Ошибки как часть типа

Функциональный подход к ошибкам переворачивает привычную модель. В языках с исключениями ошибка летит вверх по стеку, и легко забыть её поймать — тип функции ничего о ней не говорит. Try и Either делают возможную ошибку частью возвращаемого типа. Тот, кто вызывает функцию, видит в сигнатуре Either[Error, Result] и обязан явно решить, что делать с обоими исходами. Ошибку нельзя случайно проигнорировать.

Выбор между Try и Either отражает разницу между неожиданным и ожидаемым. Try хорош там, где код может бросить исключение по не зависящим от вас причинам: чтение файла, парсинг внешних данных, обращение к сети. Either лучше для ошибок бизнес-логики, где вы сами определяете осмысленный тип ошибки: «возраст отрицательный», «пользователь не найден». Оба работают в for-выражениях, поэтому несколько шагов, способных дать ошибку, соединяются в чистую цепочку с автоматическим коротким замыканием на первой неудаче.

Стоит выработать в команде единое соглашение: какие ошибки выражать через Option, какие — через Either, а какие — через Try. Последовательность здесь важнее идеального выбора: когда весь код обрабатывает ошибки в одном стиле, его проще читать и поддерживать. Главное — что ошибки выражены значениями и видны в типах, а не прячутся в исключениях, о которых легко забыть.

Итоги. Try ловит исключения в Success/Failure, Either выражает два исхода через Left/Right; оба работают в for и делают ошибки явными. Дальше — обзор экосистемы и что учить дальше.

Проверьте себя
1. Что представляет тип Try в Scala?
AТолько успешные вычисления
BРезультат, который либо Success(значение), либо Failure(исключение)
CЦикл с повторными попытками
DТип для чисел
2. По соглашению, какая сторона Either обозначает успех?
ALeft
BRight
CОбе одинаково
DНи одна