Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle exceptions in a playframework 2 Async block (scala)

My controller action code looks like this:

  def addIngredient() = Action { implicit request =>
    val boundForm = ingredientForm.bindFromRequest
    boundForm.fold(
      formWithErrors => BadRequest(views.html.Admin.index(formWithErrors)),
      value => {
        Async {
          val created = Service.addIngredient(value.name, value.description)
          created map { ingredient =>
            Redirect(routes.Admin.index()).flashing("success" -> "Ingredient '%s' added".format(ingredient.name))
          }

          // TODO on exception do the following
          // BadRequest(views.html.Admin.index(boundForm.copy(errors = Seq(FormError("", ex.getMessage())))))
        }
      })
  }

My Service.addIngredient(...) returns a Promise[Ingredient] but can also throw a custom ValidationException. When this exception is thrown I would like to return the commented code.

Currently the page renders as 500 and in the logs I have:

play - Waiting for a promise, but got an error: Ingredient with name 'test' already exists. services.ValidationException: Ingredient with name 'test' already exists.

Two questions:

  • Is it a bad idea to return this exception from my service, is there a better/more scala way to handle this case?
  • How do I catch the exception?
like image 721
Somatik Avatar asked Jun 05 '12 13:06

Somatik


1 Answers

I'd say a pure functional way would have been to use a type that can hold valid and error states.

For that, you can use Validation form scalaz

But if don't need more than that from scalaz (you will ^^), you could use a very simple stuff using a Promise[Either[String, Ingredient]] as the result and its fold method in the Async block. That is, map to convert the value when the promise is redeemed and fold on what is redeemed.

The good point => no exception => every thing is typed check :-)

EDIT

It might need a bit of information more, here are the two options: try catch, thanks to @kheraud) and Either. Didn't put the Validation, ask me if needed. object Application extends Controller {

  def index = Action {
    Ok(views.html.index("Your new application is ready."))
  }

  //Using Try Catch
  //  What was missing was the wrapping of the BadRequest into a Promise since the Async
  //    is requiring such result. That's done using Promise.pure
  def test1 = Async {
    try {
      val created = Promise.pure(new {val name:String = "myname"})
      created map { stuff =>
        Redirect(routes.Application.index()).flashing("success" -> "Stuff '%s' show".format(stuff.name))
      }
    } catch {
      case _ => {
        Promise.pure(Redirect(routes.Application.index()).flashing("error" -> "an error occurred man"))
      }
    }
  }


  //Using Either (kind of Validation)
  //  on the Left side => a success value with a name
  val success = Left(new {val name:String = "myname"})
  //  on the Right side the exception message (could be an Exception instance however => to keep the stack)
  val fail = Right("Bang bang!")

  // How to use that
  //   I simulate your service using Promise.pure that wraps the Either result
  //    so the return type of service should be Promise[Either[{val name:String}, String]] in this exemple
  //   Then while mapping (that is create a Promise around the convert content), we folds to create the right Result (Redirect in this case).
  // the good point => completely compiled time checked ! and no wrapping with pure for the error case.
  def test2(trySuccess:Boolean) = Async {
      val created = Promise.pure(if (trySuccess) success else fail)
      created map { stuff /* the either */ =>
        stuff.fold(
          /*success case*/s => Redirect(routes.Application.index()).flashing("success" -> "Stuff '%s' show".format(s.name)),
          /*the error case*/f => Redirect(routes.Application.index()).flashing("error" -> f)
        )

      }

  }

}
like image 141
Andy Petrella Avatar answered Nov 16 '22 02:11

Andy Petrella