j3d
j3d

Reputation: 9724

Play: How to improve error handling and avoid creation of unnecessary throwable instances

Creating and throwing exceptions is an expensive task... and often in web applications is not necessary to throw exceptions at all. If an error occurs in a service class used by the controller, then throwing an exception makes sense... but if the controller is already aware of the issue (e.g. user not found) it is just OK to return a JSON that describes the problem.

A solution could be an Error case class that extends Throwable only when necessary, i.e. when a service invoked by the controller fails:

object MyErrors {

  trait Error { def getMessage: String }
  final case class UserNotFound(userId: Strig) extends Error { def getMessage = s"user $userId not found" }
  final case class UserNotFoundException(userId: String) extends Throwable (s"user $userId not found") with Error
  final case class DuplicateKey(key: Strig) extends Error { def getMessage = s"key $key already exists" }
  final case class DuplicateKeyException(key: String) extends Throwable (s"key $key already exists") with Error
  ...
}

Regardless of whether or not the Error is a Throwable, I can handle possible errors like this:

object Users extends Controller {

  ...

  def find(id: String) = Action { request =>
    userService.find(id).map {
      case Some(user) => Ok(success(user.toJson))
      case None =>
        // UserNotFound implements Error but does not inherits from Throwable
        errors.toResult(UserNotFound(id))
    }.recover { case e =>
      // e is thrown by userService.find() and extends Throwable and implements Error (e.g. DuplicateKeyException)
      errors.toResult(e)
    }
  }
}

errors.toResult maps the current exception or error to the appropriate HTTP result (e.g. BadRequest, NotFound, etc.) and converts e to JSON - see this post for the complete implementation.

Now my question is: is there a less intricate way to accomplish this? As you can see I had to create two distinct case classes for a single error (in the example I repeat the error message twice just to keep things simple)...

Upvotes: 1

Views: 496

Answers (2)

Boolean
Boolean

Reputation: 21

Well, it doesn't have to be messy. Asynchronous is an 'effect" and can be stacked on top of disjunction(Either) in a way that newer Monad(think of it Monad of Monad) can be used in for comprehension. This actually enriches the algebra without changing the core behaviour or previous transformations. In order to avoid unwrapping twice, use Monad transformer such as EitherT from cats, scalaz or similar library.

In this case, it will be EitherT[Future, A, B] which internally wraps Future[Either[A,B]]. Take a look at this example: Herding cats

Also find more on stacking effects in this talk.

Upvotes: 0

johanandren
johanandren

Reputation: 11479

Two different ideas:

If the expensiveness in exceptions is about the stack trace, but you would want to use exceptions if it wasn't so expensive, there is a mixin for exception that will skip generating the stacktrace in scala.util.control.NoStackTrace

If it is more about writing clean FP code (where exceptions does not fit in and you have all possible outcomes in the return type) you could use scala.util.Either (or for less verbosity for example Or from the scalactic library, or \/ from ScalaZ).

With Either you would see Left as failure and Right for success. There then are convenience methods to transform between Option and Either.

val opt: Option[Int] = None
val either: Either[String, Int] = opt.toRight("Value for none/left")

If you use the right projection you can even do for comprehensions that will exit early if the value is not a Right.

val goodOrBad1: Either[String, Int] = Right(5)
val goodOrBad2: Either[String, Int] = Left("Bad")

val result: Either[String, Int] = for {
  good1 <- goodOrBad1.right
  good2 <- goodOrBad2.right
} yield good1 + good2

// fails with first non Right
result == Left("Bad")

Let's pretend userService.find and cartService.find returns Option[Something], that means you could do:

def showCart(id: String) = Action { request =>
  val userAndCartOrError: Either[Error, (User, Cart)] = for {
    user <- userService.find(id).toRight(UserNotFound(id)).right
    cart <- cartService.findForUser(id).toRight(NoCart(id)).right
  } yield (user, cart)

  userAndCartOrError.fold(
    error => Errors.toResult(error),
    userAndCart => Ok(Json.toJson(userAndCart))
  )
}

Of course with futures it becomes a bit more messy since you cannot mix different monads in the same for comprehension (Future and Either)

Upvotes: 2

Related Questions