ZIO : mais d'où vient cette stack trace ?

Si vous utilisez ZIO, une librairie Scala permettant de gérer votre code à effet, vous avez sûrement rencontré un jour une erreur de ce type :

scalaz.zio.Errors$UnhandledError: An error was not handled by a fiber: java.io.FileNotFoundException: Philippe (No such file or directory)
	at scalaz.zio.RTS$FiberContext.$anonfun$reportErrors$1(RTS.scala:887)
	at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:12)
	at scalaz.zio.RTS$$anon$1.run(RTS.scala:91)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "main" scalaz.zio.Errors$UnhandledError: An error was not handled by a fiber: java.io.FileNotFoundException: Philippe (No such file or directory)
	at scalaz.zio.RTS.unsafeRun(RTS.scala:25)
	at scalaz.zio.RTS.unsafeRun$(RTS.scala:21)
	at Toto$.unsafeRun(Titi.scala:5)
	at Toto$.main(Titi.scala:11)
	at Toto.main(Titi.scala)

Process finished with exit code 1
object Toto extends RTS {
  def exceptionIO(filename: String): IO[Exception, Seq[String]] = {
    IO.syncException(Source.fromFile(filename).getLines().toSeq)
  }

  def main(args: Array[String]): Unit = {
    unsafeRun(exceptionIO("Philippe"))
  }
}

Super méga chouette, nous avons une java.io.FileNotFoundException: Philippe mais rien sur l'endroit où se cache le coupable.

Philippe! Je sais où tu te caches!

La doc de ZIO

Si on essaye l'exemple de la documentation:

object Titi extends App {
  override def run(args: List[String]): IO[Nothing, Titi.ExitStatus] = {
    Toto.exceptionIO("Philippe").attempt.map(_.fold(_ => 1, _ => 0)).map(ExitStatus.ExitNow(_))
  }
}
Process finished with exit code 1

On n'est pas plus avancé !

Do It Yourself

On cherche à récupérer la stack trace d'une manière ou d'une autre pour avoir un peu plus d'informations sur la ligne de code qui nous embête.

Pour cela nous allons créer quelques fonctions (accompagnées de leur coulis d'implicit class) qui nous permettent de reporter la stack trace en console :

object Utils {

  implicit class IOOps[E, A](io: IO[E, A]) {
    def peekError(peek: E => IO[Nothing, Unit]): IO[E, A] = {
      io.flip.peek(peek).flip
    }
  }

  implicit class ExceptionOps[E <: Throwable, A](io: IO[E, A]) {
    def printException: IO[Nothing, Option[A]] = {
      io.peekError(x => IO.sync({
        x.printStackTrace()
      })).attempt.map(_.right.toOption)
    }
  }

}
object Toto extends RTS {
  def exceptionIO(filename: String): IO[Exception, Seq[String]] = {
    IO.syncException(Source.fromFile(filename).getLines().toSeq)
  }

  def main(args: Array[String]): Unit = {
    import Utils._
    unsafeRun(exceptionIO("Philippe").printException)
  }
}
java.io.FileNotFoundException: Philippe (No such file or directory)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at scala.io.Source$.fromFile(Source.scala:90)
	at scala.io.Source$.fromFile(Source.scala:75)
	at scala.io.Source$.fromFile(Source.scala:53)
	at Toto$.$anonfun$exceptionIO$1(Titi.scala:7)
	at scalaz.zio.IO$.$anonfun$syncCatch$1(IO.scala:974)
	at scalaz.zio.RTS$FiberContext.evaluate(RTS.scala:372)
	at scalaz.zio.RTS.unsafeRunSync(RTS.scala:39)
	at scalaz.zio.RTS.unsafeRunSync$(RTS.scala:37)
	at Toto$.unsafeRunSync(Titi.scala:5)
	at scalaz.zio.RTS.unsafeRun(RTS.scala:21)
	at scalaz.zio.RTS.unsafeRun$(RTS.scala:21)
	at Toto$.unsafeRun(Titi.scala:5)
	at Toto$.main(Titi.scala:11)
	at Toto.main(Titi.scala)

Voilà, maintenant nous savons à quelle ligne de code on cherchait Philippe.

Conclusion

ZIO c'est top, néanmoins il manque parfois quelques fonctions pour gérer des cas classiques.

On peut légitimement se poser la question du choix de ZIO par rapport à Cats-Effects quand on fait ce genre de "bêtises", mais nous n'avons pas la place dans cet article pour en discuter ! 😃