How to handle errors elegantly?

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:

How to use implicit to unify our errors?

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
  }
}

What happens after using the attempt method?

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.

The Completed codes:

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
Last updated: 2024-03-30 10:49:01Scala/HTTP4s/Cats
Author:Chaolocation:https://www.baidu.com/article/26
Comments
Submit
Be the first one to write a comment ~