zio-grpc icon indicating copy to clipboard operation
zio-grpc copied to clipboard

How to catch exceptions?

Open omidb opened this issue 2 years ago • 6 comments

I have a ZIO grpc server running and somewhere in my code there is an ??? unimplemented exception. I get now cause or anything in my call and I cannot log the error or cause. Using the debugger I see that it ends up in here: https://github.com/scalapb/zio-grpc/blob/5c3b4b8b0e186138609da4eb5fb9cb97120db16a/core/src/main/scalajvm/scalapb/zio_grpc/server/ListenerDriver.scala#L32

of course, wrapping everything in ZIO.attempt bubbles up the error but that is not a good pattern to follow.

What am I missing?

omidb avatar Aug 15 '23 23:08 omidb

If I understand correctly, you are looking for a way to handle exceptions systematically throughout your app. Have you looked into accomplishing this with a ZTransform similar to the one here: https://scalapb.github.io/zio-grpc/docs/next/decorating

thesamet avatar Aug 16 '23 00:08 thesamet

here is a better version of my sample using the samples in the repo:

package zio_grpc.examples.helloworld

import io.grpc.StatusException
import scalapb.zio_grpc.ServerMain
import scalapb.zio_grpc.ServiceList
import zio._
import zio.Console._

import io.grpc.examples.helloworld.helloworld.ZioHelloworld.Greeter
import io.grpc.examples.helloworld.helloworld.{HelloReply, HelloRequest}

import io.grpc.StatusException
import scalapb.zio_grpc.{RequestContext, ZTransform}
import zio._
import zio.stream.ZStream

class LoggingTransform extends ZTransform[Any, RequestContext] {

  def logCause(rc: RequestContext, cause: Cause[StatusException]): UIO[Unit] =
    printLine(rc.toString()).orDie

  def accessLog(rc: RequestContext): UIO[Unit] = {
    printLine(rc.toString()).orDie
  }

  override def effect[A](
      io: Any => ZIO[Any, StatusException, A]
  ): RequestContext => ZIO[Any, StatusException, A] = { rc =>
    io(rc).zipLeft(accessLog(rc)).tapErrorCause(logCause(rc, _))
  }

  override def stream[A](
      io: Any => ZStream[Any, StatusException, A]
  ): RequestContext => ZStream[Any, StatusException, A] = { rc =>
    (io(rc) ++ ZStream.fromZIO(accessLog(rc)).drain).onError(logCause(rc, _))
  }
}

object GreeterImpl extends Greeter {

  def parse(value: String) = {
    value match {
      case "test" => ???
      case "fail" =>
        ZIO.fail(new StatusException(io.grpc.Status.INVALID_ARGUMENT))
      case _ =>
        ZIO.succeed(HelloReply(s"Parsed, ${value}"))
    }
  }
  def sayHello(
      request: HelloRequest
  ): ZIO[Any, StatusException, HelloReply] = {

    parse(request.name).logError("errr") *> printLine(
      s"Got request: $request"
    ).orDie zipRight
      ZIO.succeed(HelloReply(s"Hello, ${request.name}"))
  }
}

object HelloWorldServer extends ServerMain {

  val decoratedService =
    GreeterImpl.transform(new LoggingTransform)
  def services: ServiceList[Any] = ServiceList.add(decoratedService)
}

@thesamet I tested with and without transform.

basically there is no way to log non-zio exceptions. In a simple Zio code you get an exception in the console. In Akka or other places, we get the exception in the console or logs and server continue to work. Is my question more clear now?

omidb avatar Aug 16 '23 21:08 omidb

I'd like to investigate more on what would be a consistent behavior with other ZIO libraries, however in the meantime, if you expect your effects might throw exception maybe the transformer can catch, handle and convert them to values:

  override def effect[A](
     io: Any => ZIO[Any, StatusException, A]
  ): RequestContext => ZIO[Any, StatusException, A] = { rc =>
    try { io(rc) }
    catch {
      case t: Throwable =>
        printLine(t.toString) *> ZIO.fail(t)
    }
  }

thesamet avatar Aug 17 '23 12:08 thesamet

yep, this does the job:

override def effect[A](
      io: Any => ZIO[Any, StatusException, A]
  ): RequestContext => ZIO[Any, StatusException, A] = { rc =>
    try { io(rc) }
    catch {
      case t: Throwable =>
        (ZIO.logError(t.getMessage()) *> ZIO.fail(t)).mapError { t =>
          new StatusException(Status.INTERNAL)
        }

    }
  }

omidb avatar Aug 17 '23 15:08 omidb

Nice. This probably gives reasonable coverage for most people but it's worth noting that few cases will not be caught, for instance if io(rc) returns null or an object that's not a ZIO. Those would be passed through and trigger the error outside the try{}

On Thu, Aug 17, 2023 at 8:48 AM Omid Bakhshandeh @.***> wrote:

yep, this does the job:

override def effect[A]( io: Any => ZIO[Any, StatusException, A] ): RequestContext => ZIO[Any, StatusException, A] = { rc => try { io(rc) } catch { case t: Throwable => (ZIO.logError(t.getMessage()) *> ZIO.fail(t)).mapError { t => new StatusException(Status.INTERNAL) }

}

}

— Reply to this email directly, view it on GitHub https://github.com/scalapb/zio-grpc/issues/543#issuecomment-1682517486, or unsubscribe https://github.com/notifications/unsubscribe-auth/AACLBLOL3JMM4XL5NCXUIHDXVY4OHANCNFSM6AAAAAA3RW2OYY . You are receiving this because you were mentioned.Message ID: @.***>

-- -Nadav

thesamet avatar Aug 17 '23 17:08 thesamet

@thesamet it would be great to have a catch all to at least be able to log everything

omidb avatar Aug 17 '23 20:08 omidb