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 и делают ошибки явными. Дальше — обзор экосистемы и что учить дальше.