Why does Future's recover not catch exceptions?
Asked Answered
H

2

22

I'm using Scala, Play Framework 2.1.x, and reactivemongo driver.

I have an api call :

def getStuff(userId: String) = Action(implicit request => {
    Async {
      UserDao().getStuffOf(userId = userId).toList() map {
        stuffLst => Ok(stuffLst)
      } 
    }
})

It works fine 99% of the time but it may fail sometimes (doesn't matter why, that's not the issue).

I wanted to recover in a case of an error so i added:

recover { case _ => BadRequest("")}

But this does not recover me from errors.
I tried the same concept on the scala console and it worked:

import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
var f = future { throw new Exception("") } map {_ => 2} recover { case _ => 1}
Await.result(f, 1 nanos)

This returns 1 as expected.
I currently wrapped the Async with:

try{
  Async {...}
} catch {
  case _ => BadRequest("")
} 

And this catches the errors.

I went over some Scala's Future docs on the net and I'm baffled why recover did not work for me.

Does anyone know why? What do I miss to sort it out?

Histoid answered 17/10, 2013 at 16:8 Comment(0)
A
47

Why it fails actually matters 100%. If we spread the code over a number of lines of code, you'll understand why:

def getStuff(userId: String) = Action(implicit request => {
  Async {
    val future = UserDao().getStuffOf(userId = userId).toList()
    val mappedFuture = future.map {
      stuffLst => Ok(stuffLst)
    }
    mappedFuture.recover { case _ => BadRequest("")}
  }
})

So, UserDao().getStuffOf(userId = userId).toList() returns you a future. A future represents something that may not have happened yet. If that thing throws an exception, you can handle that exception in recover. However, in your case, the error is happening before the future is even being created, the UserDao().getStuffOf(userId = userId).toList() call is throwing an exception, not returning a future. So the call to recover the future will never be executed. It's equivalent to doing this in the Scala repl:

import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
var f = { throw new Exception(""); future { "foo" } map {_ => 2} recover { case _ => 1} }
Await.result(f, 1 nanos) }

Obviously that doesn't work, since you never created the future in the first place beacuse the exception was thrown before the code to create the future happened.

So the solution is to either wrap your call to UserDao().getStuffOf(userId = userId).toList() in a try catch block, or find out why it's failing in whatever method you're calling, and catch the exception there, and return a failed future.

Arboreous answered 18/10, 2013 at 3:43 Comment(2)
what exactly is a "failed future" ?Birl
@ps0604: Future.failed(<the exception you caught>)Smitherman
L
4

If you have a later version of Play eg 2.2.x, you can do this:

def urlTest() = Action.async {
    val holder: WSRequestHolder = WS.url("www.idontexist.io")
    holder.get.map {
      response =>
        println("Yay, I worked")
        Ok
    }.recover {
      case _ =>
        Log.error("Oops, not gonna happen")
        InternalServerError("Failure")
    }
}
Ligate answered 24/3, 2014 at 0:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.