Scala, ZIO - how to return custom error from ZIO

677 views Asked by At

I have a simple code which using tapir sttp.client3.json._:

def method(...): Task[MyResponse] = 
   someLogic().response(asJson[MyResponse]).flatMap(res => ZIO.fromEither(res.body))

But now I want to add here some logic to use custom errors when asJson[MyResponse] failed. I created my error hierarchy:

sealed trait MyError extends Throwable
case class MyFirstError (msg: String) extends MyError 
case class MySecondError (msg: String) extends MyError 

And I changed Task to IO[MyError, MyResponse], but I have no idea how should I return errors when they appeared and return same correct MyResponse when everything is ok. I tried smth like this:

def method(...): IO[MyError, MyResponse] = 
       someLogic().response(asJson[MyResponse]).flatMap{ res =>
         res.code match {
           case StatusCode.BadRequest => ZIO.fail(MyFristError("my error"))
           case StatusCode.Forbidden => ZIO.fail(MySecondError("my other error"))
           case _ => ZIO.fromEither(res.body).orDie
         }
       }

I got an error here Required IO[MyError, MyResponse], Found ZIO[Any, Throwable, MyResponse] How should I fix this code to return my custom errors depend on the status code and correct response when everything is ok?

2

There are 2 answers

2
Austinito On

I think you're running into this issue on the "catch-all" case:

  case _ => ZIO.fromEither(res.body).orDie

The fromEither is returning some IO[E, A] my assumption is that the E is where you're running into the Throwable. Explicity declaring the types should fix it: fromEither[MyError, MyResponse].

If adding the types explicity isn't possible, then instead of using orDie, you could use refineOrDie { case e: MyError => e }.

0
Sergey Romanovsky On

The signature defines what kind of errors your method can produce. orDie terminates the fiber (with any Throwable, not necessarily MyError) so you can

  1. either catchall all exceptions and map to your trait:
sealed trait MyError extends Throwable
case class MyFirstError (msg: String) extends MyError 
case class MySecondError (msg: String) extends MyError 
case class UnexpectedResponseCode(msg: String) extends MyError
def method(...): IO[MyError, MyResponse] = 
  someLogic().response(asJson[MyResponse]).flatMap{ res =>
    res.code match {
      case StatusCode.BadRequest => ZIO.fail(MyFristError("my error"))
      case StatusCode.Forbidden => ZIO.fail(MySecondError("my other error"))
      case unexpectedResponseCode => ZIO.fail(UnexpectedResponseCode(s"I.I. Rabi> Who ordered that '$unexpectedResponseCode'?")
    }
  }
}
  1. Or make the signature more general, i.e.
def method(...): Task[MyResponse] // AKA IO[Throwable, MyResponse]