Как работает Try и чем он полезен?

В Scala Try — это контейнер для обработки кода, который может выбросить исключение, без явного использования конструкции try-catch. Он позволяет выразить возможность ошибки через тип, а не через побочный эффект (вброс исключения), что делает его более подходящим для функционального стиля программирования.

Что такое Try

Try — это алгебраический тип данных, имеющий два подкласса:

  • Success(value) — успешный результат выполнения

  • Failure(exception) — неудача, содержащая выброшенное исключение

Тип Try[T] — это результат выполнения некоторого кода, который может либо успешно вернуть значение типа T, либо завершиться с исключением.

Пример использования

import scala.util.{Try, Success, Failure}
val result = Try {
val x = 10 / 0
x
}

В данном примере результат будет:

Failure(java.lang.ArithmeticException: / by zero)

Если же код отработал успешно:

val result = Try {
val x = 10 / 2
x
}
// Success(5)

Почему Try полезен

  1. Избегает традиционного try-catch, делает ошибки частью типа.

  2. Позволяет комбинировать вычисления без вложенности.

  3. Имеет API, совместимый с Option, Either, Future — поддерживает map, flatMap, filter, getOrElse, recover.

  4. Подходит для композиции цепочек возможных неудачных операций.

Основные методы Try

map

Применяет функцию к значению внутри Success, Failure остаётся неизменным:

val t = Try(10 / 2).map(_ \* 2) // Success(10)

flatMap

Позволяет вызывать цепочки, возвращающие другие Try:

def parseInt(s: String): Try\[Int\] = Try(s.toInt)
val res = parseInt("10").flatMap(x => Try(100 / x)) // Success(10)
val fail = parseInt("not_a_number").flatMap(x => Try(100 / x)) // Failure(NumberFormatException)

getOrElse

Возвращает значение или значение по умолчанию:

Try(1 / 0).getOrElse(42) // 42

recover

Позволяет восстановить значение из ошибки:

val r = Try(1 / 0).recover {
case \_: ArithmeticException => 0
} // Success(0)

recoverWith

Как recover, но возвращает другой Try:

val r = Try(1 / 0).recoverWith {
case \_: ArithmeticException => Success(42)
}

fold

Универсальное свёртывание:

Try(10 / 2).fold(
ex => s"Failed: ${ex.getMessage}",
value => s"Success: $value"
)
// "Success: 5"

Использование в цепочках

val result = for {
a <- Try("100".toInt)
b <- Try(10 / a)
} yield b
// Success(0)

Если в любой строке возникает ошибка, всё выражение прерывается и возвращается Failure.

Преобразование Try

  • В Option:
Try(5).toOption // Some(5)
Try(throw new Exception).toOption // None
  • В Either:
Try(5).toEither // Right(5)
Try(throw new Exception("fail")).toEither // Left(Exception)
  • Из Either обратно:
val e: Either\[Throwable, Int\] = Right(5)
val t: Try\[Int\] = Try(e.getOrElse(throw e.left.get))

Обработка ошибок

Try {
someUnsafeOperation()
}.recover {
case ex: IOException => defaultValue
}

Это особенно полезно, если ты хочешь обернуть весь блок кода и дальше работать с ним как с обычным значением.

Когда использовать Try, а не Either?

Сценарий Использовать
Ошибка с Throwable Try
--- ---
Явная обработка ошибок через тип Either
--- ---
Использование исключений Try
--- ---
Предпочтение функциональному стилю Either
--- ---

В большинстве случаев Try хорош для обёртки кода с исключениями, а Either — для более строгого контроля ошибок, особенно в бизнес-логике.

Внутренняя реализация Try

Упрощённо:

sealed trait Try\[+T\] {
def isSuccess: Boolean
def isFailure: Boolean
...
}
final case class Success\[+T\](value: T) extends Try\[T\]
final case class Failure\[+T\](exception: Throwable) extends Try\[T\]

Это облегчает сопоставление с образцом:

Try(10 / 0) match {
case Success(v) => println(s"Got: $v")
case Failure(e) => println(s"Error: ${e.getMessage}")
}

Пример: безопасное деление

def safeDivide(a: Int, b: Int): Try\[Int\] = Try(a / b)
val result = safeDivide(10, 0).recover {
case \_: ArithmeticException => 0
}

Пример: загрузка файла

def loadConfig(): Try\[String\] = Try {
val source = scala.io.Source.fromFile("config.txt")
try source.getLines().mkString("\\n")
finally source.close()
}
loadConfig().foreach(println)

Объединение нескольких Try

val t1 = Try(1)
val t2 = Try(2)
val t3 = Try(3)
val total = for {
a <- t1
b <- t2
c <- t3
} yield a + b + c

Если один из Try — Failure, весь результат — Failure.

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