Как обрабатывать ошибки в Future?

В Scala Future может завершиться как успешно, так и с ошибкой. Ошибка, возникшая во время выполнения кода внутри Future, не выбрасывается как исключение в обычном смысле, а оборачивается в Failure(Throwable) и хранится внутри Future. Это делает работу с Future безопасной и предсказуемой. Обработка таких ошибок осуществляется с помощью набора функциональных конструкций: onComplete, recover, recoverWith, fallbackTo, а также через try-catch внутри тела Future.

Общая схема

import scala.concurrent._
import scala.util.{Success, Failure}
import scala.concurrent.ExecutionContext.Implicits.global
val f = Future {
if (scala.util.Random.nextBoolean()) 42
else throw new RuntimeException("Boom!")
}

1. onComplete — универсальный обработчик

Вызывает функцию после завершения Future, независимо от результата (успех/ошибка):

f.onComplete {
case Success(value) => println(s"Got value: $value")
case Failure(ex) => println(s"Error occurred: ${ex.getMessage}")
}

Это наиболее прямой способ отследить ошибку, но не возвращает новый Future, поэтому не подходит для трансформаций.

2. recover — обрабатывает ошибку и возвращает значение

Позволяет заменить ошибку на нормальный результат:

val safe = f.recover {
case \_: RuntimeException => 0
}

Если f завершится с ошибкой RuntimeException, safe станет Future.successful(0). Если ошибка другая — она пробрасывается дальше.

3. recoverWith — обрабатывает ошибку и возвращает другой Future

Полезно, когда необходимо выполнить альтернативное асинхронное действие:

val safeAlt = f.recoverWith {
case \_: RuntimeException => Future.successful(100)
}

Если f упал, safeAlt становится Future(100).

4. fallbackTo — подставляет другой Future при ошибке

Более жёсткий вариант recoverWith: если основной Future упал, выполняется запасной:

val primary = Future.failed\[Int\](new RuntimeException("fail"))
val backup = Future.successful(10)
val result = primary.fallbackTo(backup) // будет 10

Если оба Future упадут — ошибка будет из второго (backup).

5. Вложенный try-catch внутри Future

Ошибки, выброшенные синхронно внутри Future, автоматически преобразуются в Failure, но можно использовать try-catch, если нужно явно перехватить исключение:

val f = Future {
try {
val result = someDangerousCode()
result
} catch {
case ex: Throwable =>
println("Caught manually")
throw ex
}
}

Однако чаще предпочтительнее использовать recover или onComplete.

6. transform и transformWith

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

val transformed = f.transform(
s => s \* 2, // при успехе
e => new RuntimeException("Wrapped: " + e.getMessage) // при ошибке
)

transformWith позволяет возвращать новый Future:

val chained = f.transformWith {
case Success(value) => Future.successful(value \* 2)
case Failure(\_) => Future.successful(0)
}

7. Комбинация map и recover

Можно сначала применить map, а затем подстраховаться:

val f = Future(100 / 0)
val handled = f.map(_ + 1).recover {
case \_: ArithmeticException => -1
}

8. Логирование ошибок

Ошибки можно логировать через foreach или onComplete:

f.failed.foreach { ex =>
println(s"Future failed: ${ex.getMessage}")
}

failed возвращает Future[Throwable], если исходный Future упал.

9. Пример: цепочка Future с ошибками

val f1 = Future(10)
val f2 = f1.map(_ / 0) // ошибка
val f3 = f2.recover {
case \_: ArithmeticException => 0
}

f3 получит значение 0, несмотря на ошибку в f2.

10. Пример: цепочка с fallback

val mainCall = Future {
throw new RuntimeException("Service down")
}
val backupCall = Future.successful("cached result")
val result = mainCall.fallbackTo(backupCall)

В случае сбоя основного запроса результатом станет "cached result".

Важные замечания

  • Все recover и fallbackTo не вызываются, если Future завершился успешно.

  • Ошибки оборачиваются в scala.util.Failure.

  • Ошибки в onComplete, map, flatMap и других методах, брошенные синхронно, тоже попадут в Failure.

Паттерн безопасного исполнения

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

Такой подход позволяет не бояться крашей и получать предсказуемое поведение при сбое.