We may encounter different types of errors. We want to handle them at the end at one time. We usually make transformations to the error types to unify the error types and convert the errors or values into responses eventually.
For example, when we receive a user name, we raise an intentional error when the user name is Jason, otherwise, return welcome <user>
as a response.
Some of the cases should be considered:
Define our error type
We create a case class called TMError
to make it extend Exception
and our defined trait TMInfo
sealed trait TMInfo
case class TMError(description: String, errorType: ErrorType = IntentionError) extends Exception with TMInfo
implicit class ThrowableToTMError(t: Throwable) {
def toTMError: TMError = t match {
case v: TMError => v
case _ =>
TMError(t.getMessage)
}
}
For the parameter, we also pass one's type called ErrorType. This is useful for our definition of specified error types.
sealed trait ErrorType
case object IntentionError extends ErrorType
When we want to handle an object that could be an error or value, we can write an implicit class containing a method to convert to a response implicitly.
implicit class TMErrorEitherToResponse[M[_] : Monad, T: Encoder](result: M[TMErrorEither[T]]) extends Http4sDsl[M] {
def toResponse: M[Response[M]] = result.flatMap {
case Left(result@TMError(_, IntentionError)) => BadRequest(result)
case Left(result) => InternalServerError(result)
case Right(value) => Ok(value)
}
}
As we noticed, T
is Encoder
type, which means we also need to define an implicit object containing a method to encode T
value.
implicit object ResultEncoder extends Encoder[TMError] {
override def apply(a: TMError): Json = {
Json.obj(
("description", a.description.asJson),
("errorType", a.errorType.toString.asJson)
).dropNullValues
}
}
The defined methods above can be used as follows. Be aware that we need to import implicit by using ._
def welcome[M[_] : Sync](req: Option[String]): M[Response[M]] = {
(for {
_ <- Sync[M].raiseError(TMError("User is Jason", IntentionError)).whenA(req.contains("Jason"))
user = req.map(username => s"Welcome, $username").getOrElse("No user provided")
} yield user).attempt.map(x => x.leftMap(_.toTMError)).flatMap[TMErrorEither[String]] {
case Left(e) => Sync[M].delay {
println(e);
Left(e)
}
case value => Sync[M].pure(value)
}
.toResponse
}
}
The attempt
method will transfor M[A]
into M[Either[Throwable, A]]
, such as transfor M[Unit]
into M[Either[Throwable,Unit]
. Then use map
to take out Either[Throwable,A]
and transfer the left value which is Throwable
into our defined Error type TMError
. Either
does not have leftMap
method but cats extend its ability, so we need to import cats.implicits._
.
The print error logic can be replaced to log it in the future.
Main.scala
import TMInfo.{IntentionError, TMError, TMErrorEither, ThrowableToTMError, _}//need to add _ to import toResponse method
import cats.effect.unsafe.implicits.global
import cats.effect.{IO, Sync}
import cats.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.dsl.io._
import org.http4s.dsl.request.Root
import org.http4s.{HttpRoutes, Response}
import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.DurationInt
import scala.language.higherKinds
object Main {
def main(args: Array[String]): Unit = {
(for {
serverExecutionContext <- IO(
ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1))
)
routes = HttpRoutes.of[IO] {
case GET -> Root / "welcome" / user =>
welcome(Some(user))
}
serverIO <- BlazeServerBuilder[IO]
.withExecutionContext(serverExecutionContext)
.withIdleTimeout(180.seconds)
.bindHttp(port = 8080, host = "0.0.0.0")
.withHttpApp(routes.orNotFound)
.serve
.compile
.drain
} yield serverIO).unsafeRunSync()
}
def welcome[M[_] : Sync](req: Option[String]): M[Response[M]] = {
(for {
_ <- Sync[M].raiseError(TMError("User is Jason", IntentionError)).whenA(req.contains("Jason"))
user = req.map(username => s"Welcome, $username").getOrElse("No user provided")
} yield user).attempt.map(x => x.leftMap(_.toTMError)).flatMap[TMErrorEither[String]] {
case Left(e) => Sync[M].delay {
println(e);
Left(e)
}
case value => Sync[M].pure(value)
}
.toResponse
}
}
TMInfo.scala
import cats.Monad
import cats.implicits._
import io.circe.syntax.EncoderOps
import io.circe.{Encoder, Json}
import org.http4s.Response
import org.http4s.dsl.Http4sDsl
import org.http4s.circe.CirceEntityCodec.circeEntityEncoder
import scala.language.higherKinds
object TMInfo {
type TMErrorEither[T] = Either[TMError, T]
sealed trait ErrorType
case object IntentionError extends ErrorType
sealed trait TMInfo
case class TMError(description: String, errorType: ErrorType = IntentionError) extends Exception with TMInfo
implicit class ThrowableToTMError(t: Throwable) {
def toTMError: TMError = t match {
case v: TMError => v
case _ =>
TMError(t.getMessage)
}
}
implicit object ResultEncoder extends Encoder[TMError] {
override def apply(a: TMError): Json = {
Json.obj(
("description", a.description.asJson),
("errorType", a.errorType.toString.asJson)
).dropNullValues
}
}
implicit class TMErrorEitherToResponse[M[_] : Monad, T: Encoder](result: M[TMErrorEither[T]]) extends Http4sDsl[M] {
def toResponse: M[Response[M]] = result.flatMap {
case Left(result@TMError(_, IntentionError)) => BadRequest(result)
case Left(result) => InternalServerError(result)
case Right(value) => Ok(value)
}
}
}
Start the server and try it out:
curl -vv http://localhost:8080/welcome/john
curl -vv http://localhost:8080/welcome/Jason