Repository: 7mind/izumi
Pull request #1230 (2020-09-18 17:39:40)

Allow non-Throwable errors in `BIOError#withFilter`

codecov-commenter: # [Codecov](https://codecov.io/gh/7mind/izumi/pull/1230?src=pr&el=h1) Report
> Merging [#1230](https://codecov.io/gh/7mind/izumi/pull/1230?src=pr&el=desc) into [develop](https://codecov.io/gh/7mind/izumi/commit/1bf8578b4800d27a63562390989bb6208b5dbf1b?el=desc) will **increase** coverage by `0.19%`.
> The diff coverage is `21.15%`.


```diff
@@             Coverage Diff             @@
##           develop    #1230      +/-   ##
===========================================
+ Coverage    54.60%   54.80%   +0.19%     
===========================================
  Files          419      418       -1     
  Lines         7823     7638     -185     
  Branches       399      397       -2     
===========================================
- Hits          4272     4186      -86     
+ Misses        3551     3452      -99     
```


Patch:
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOError3.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOError3.scala
index bb0a51c08..8049dceae 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOError3.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOError3.scala
@@ -27,12 +27,22 @@ trait BIOError3[F[-_, +_, +_]] extends BIOApplicativeError3[F] with BIOMonad3[F]
   def tapBoth[R, E, A, E1 >: E](r: F[R, E, A])(err: E => F[R, E1, Unit], succ: A => F[R, E1, Unit]): F[R, E1, A] = {
     tap(tapError[R, E, A, E1](r)(err))(succ)
   }
+
   /** for-comprehensions sugar:
     *
     * {{{
     *   for {
-    *    (1, 2) <- F.pure((2, 1))
+    *     (1, 2) <- F.pure((2, 1))
+    *   } yield ()
+    * }}}
+    *
+    * Use [[widenError]] to for pattern matching with non-Throwable errors:
+    *
+    * {{{
+    *   val f = for {
+    *     (1, 2) <- F.pure((2, 1)).widenError[Option[Unit]]
     *   } yield ()
+    *   // f: F[Option[Unit], Unit] = F.fail(Some(())
     * }}}
     */
   @inline final def withFilter[R, E, A](r: F[R, E, A])(predicate: A => Boolean)(implicit filter: BIOWithFilter[E], pos: SourceFilePositionMaterializer): F[R, E, A] = {
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOWithFilter.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOWithFilter.scala
new file mode 100644
index 000000000..c6c16c62f
--- /dev/null
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/BIOWithFilter.scala
@@ -0,0 +1,33 @@
+package izumi.functional.bio
+
+import izumi.fundamentals.platform.language.SourceFilePosition
+
+trait BIOWithFilter[+E] {
+  def error(matchedValue: Any, pos: SourceFilePosition): E
+}
+
+object BIOWithFilter extends BIOWithFilterInstances1 {
+  @inline def apply[E: BIOWithFilter]: BIOWithFilter[E] = implicitly
+
+  implicit def BIOWithFilterNoSuchElementException: BIOWithFilter[NoSuchElementException] = {
+    (matchedValue, pos) =>
+      new NoSuchElementException(s"Pattern match failed at $pos for value=$matchedValue")
+  }
+}
+
+sealed trait BIOWithFilterInstances1 extends BIOWithFilterInstances2 {
+  implicit def BIOWithFilterUnit: BIOWithFilter[Unit] = (_, _) => ()
+}
+
+sealed trait BIOWithFilterInstances2 extends BIOWithFilterInstances3 {
+  implicit def BIOWithFilterString: BIOWithFilter[String] = {
+    (matchedValue, pos) =>
+      s"Pattern match failed at $pos for value=$matchedValue"
+  }
+}
+
+sealed trait BIOWithFilterInstances3 {
+  implicit def BIOWithFilterOption[E](implicit filter: BIOWithFilter[E]): BIOWithFilter[Some[E]] = {
+    (matchedValue, pos) => Some(filter.error(matchedValue, pos))
+  }
+}
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala
index ddad9dab4..7a8bd3722 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncMonix.scala
@@ -52,12 +52,6 @@ class BIOAsyncMonix extends BIOAsync[IO] {
   override final def redeem[R, E, A, E2, B](r: IO[E, A])(err: E => IO[E2, B], succ: A => IO[E2, B]): IO[E2, B] = r.redeemWith(err, succ)
   override final def catchAll[R, E, A, E2](r: IO[E, A])(f: E => IO[E2, A]): IO[E2, A] = r.onErrorHandleWith(f)
   override final def catchSome[R, E, A, E1 >: E](r: IO[E, A])(f: PartialFunction[E, IO[E1, A]]): IO[E1, A] = r.onErrorRecoverWith(f)
-  override final def withFilter[R, E, A](r: IO[E, A])(predicate: A => Boolean)(implicit ev: NoSuchElementException <:< E): IO[E, A] =
-    r.flatMap {
-      a =>
-        if (predicate(a)) IO.pure(a)
-        else IO.raiseError(ev(new NoSuchElementException("The value doesn't satisfy the predicate")))
-    }
 
   override final def guarantee[R, E, A](f: IO[E, A], cleanup: IO[Nothing, Unit]): IO[E, A] = f.guarantee(cleanup)
   override final def attempt[R, E, A](r: IO[E, A]): IO[Nothing, Either[E, A]] = r.attempt
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
index c571904b4..46a4cd98a 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/impl/BIOAsyncZio.scala
@@ -51,9 +51,6 @@ class BIOAsyncZio extends BIOAsync3[ZIO] with BIOLocal[ZIO] {
   @inline override final def redeem[R, E, A, E2, B](r: ZIO[R, E, A])(err: E => ZIO[R, E2, B], succ: A => ZIO[R, E2, B]): ZIO[R, E2, B] = r.foldM(err, succ)
   @inline override final def catchAll[R, E, A, E2](r: ZIO[R, E, A])(f: E => ZIO[R, E2, A]): ZIO[R, E2, A] = r.catchAll(f)
   @inline override final def catchSome[R, E, A, E1 >: E](r: ZIO[R, E, A])(f: PartialFunction[E, ZIO[R, E1, A]]): ZIO[R, E1, A] = r.catchSome(f)
-  @inline override final def withFilter[R, E, A](r: ZIO[R, E, A])(predicate: A => Boolean)(implicit ev: NoSuchElementException <:< E): ZIO[R, E, A] = {
-    new ZIOWithFilterOps(r).withFilter(predicate)(ZIO.CanFilter.canFilter[Any].asInstanceOf[CanFilter[E]])
-  }
 
   @inline override final def guarantee[R, E, A](f: ZIO[R, E, A], cleanup: ZIO[R, Nothing, Unit]): ZIO[R, E, A] = f.ensuring(cleanup)
   @inline override final def attempt[R, E, A](r: ZIO[R, E, A]): ZIO[R, Nothing, Either[E, A]] = r.either
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIO3Syntax.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIO3Syntax.scala
index ade589c3b..7ed3d7b4d 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIO3Syntax.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIO3Syntax.scala
@@ -115,9 +115,18 @@ object BIO3Syntax {
       *
       * {{{
       *   for {
-      *    (1, 2) <- F.pure((2, 1))
+      *     (1, 2) <- F.pure((2, 1))
       *   } yield ()
       * }}}
+      *
+      * Use [[widenError]] to for pattern matching with non-Throwable errors:
+      *
+      * {{{
+      *   val f = for {
+      *     (1, 2) <- F.pure((2, 1)).widenError[Option[Unit]]
+      *   } yield ()
+      *   // f: F[Option[Unit], Unit] = F.fail(Some(())
+      * }}}
       */
     @inline final def withFilter[E1 >: E](predicate: A => Boolean)(implicit filter: BIOWithFilter[E1], pos: SourceFilePositionMaterializer): FR[R, E1, A] =
       F.withFilter[R, E1, A](r)(predicate)
diff --git a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIOSyntax.scala b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIOSyntax.scala
index 0f13b52d5..9a4bcbd1e 100644
--- a/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIOSyntax.scala
+++ b/fundamentals/fundamentals-bio/src/main/scala/izumi/functional/bio/syntax/BIOSyntax.scala
@@ -97,9 +97,18 @@ object BIOSyntax {
       *
       * {{{
       *   for {
-      *    (1, 2) <- F.pure((2, 1))
+      *     (1, 2) <- F.pure((2, 1))
       *   } yield ()
       * }}}
+      *
+      * Use [[widenError]] to for pattern matching with non-Throwable errors:
+      *
+      * {{{
+      *   val f = for {
+      *     (1, 2) <- F.pure((2, 1)).widenError[Option[Unit]]
+      *   } yield ()
+      *   // f: F[Option[Unit], Unit] = F.fail(Some(())
+      * }}}
       */
     @inline final def withFilter[E1 >: E](predicate: A => Boolean)(implicit filter: BIOWithFilter[E1], pos: SourceFilePositionMaterializer): F[E1, A] =
       F.withFilter[Any, E1, A](r)(predicate)
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/Log.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/Log.scala
index 713937af7..dfbb55fca 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/Log.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/Log.scala
@@ -69,10 +69,16 @@ object Log {
 
   }
 
-  final case class LogArg(path: Seq[String], value: Any, hiddenName: Boolean, codec: Option[LogstageCodec[_]]) {
+  final case class LogArgTyped[T](path: Seq[String], value: T, hiddenName: Boolean, codec: Option[LogstageCodec[T]]) {
     def name: String = path.last
   }
 
+  type LogArg = LogArgTyped[Any]
+  object LogArg {
+    def apply[T](value: Seq[String], t: T, hiddenName: Boolean, codec: Option[LogstageCodec[T]]): LogArg = {
+      LogArgTyped(value, t, hiddenName, codec.map(t => t.asInstanceOf[LogstageCodec[Any]]))
+    }
+  }
   type LogContext = Seq[LogArg]
 
   final case class CustomContext(values: LogContext) {
@@ -82,7 +88,7 @@ object Log {
   object CustomContext {
     def fromMap(map: Map[String, AnyEncoded]): CustomContext = {
       val logArgs = map.map {
-        case (k, v) => LogArg(Seq(k), v.value, hiddenName = false, v.codec)
+        case (k, v) => LogArg(Seq(k), v.value, hiddenName = false, v.codec.map(_.asInstanceOf[LogstageCodec[Any]]))
       }.toList
 
       CustomContext(logArgs)
@@ -136,6 +142,18 @@ object Log {
       message.args.map(_.value).collectFirst { case t: Throwable => t }
     }
 
+    def throwables: Seq[LogArgTyped[Throwable]] = {
+      message
+        .args
+        .map {
+          a => (a.value, a)
+        }
+        .collect {
+          case (t: Throwable, c) =>
+            LogArgTyped(c.path, t, c.hiddenName, c.codec.map(_.asInstanceOf[LogstageCodec[Throwable]]))
+        }
+    }
+
     @inline def addCustomContext(ctx: CustomContext): Entry = {
       if (ctx.values.isEmpty) {
         this
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/AnyEncoded.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/AnyEncoded.scala
index f820395cc..87b234a5c 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/AnyEncoded.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/AnyEncoded.scala
@@ -3,7 +3,7 @@ package izumi.logstage.api.rendering
 import scala.language.implicitConversions
 
 sealed trait AnyEncoded {
-  type T
+  type T <: Any
   def value: T
   def codec: Option[LogstageCodec[T]]
 }
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/ExtendedLogstageWriter.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/ExtendedLogstageWriter.scala
new file mode 100644
index 000000000..5be5c3dc2
--- /dev/null
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/ExtendedLogstageWriter.scala
@@ -0,0 +1,3 @@
+package izumi.logstage.api.rendering
+
+trait ExtendedLogstageWriter[T] extends LogstageWriter with LogstageWriterResult[T]
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageCodec.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageCodec.scala
index 839f397c4..712985696 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageCodec.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageCodec.scala
@@ -6,6 +6,8 @@ import izumi.fundamentals.platform.exceptions.IzThrowable._
 trait LogstageCodec[-T] {
   def write(writer: LogstageWriter, value: T): Unit
 
+  def makeReprWriter(colored: Boolean): ExtendedLogstageWriter[String] = new LogstageReprWriter(colored)
+
   final def contramap[U](f: U => T): LogstageCodec[U] = (w, v) => write(w, f(v))
 }
 
@@ -62,20 +64,13 @@ sealed trait LogstageCodecLowPriority {
   implicit final lazy val LogstageCodecDouble: LogstageCodec[Double] = _.write(_)
   implicit final lazy val LogstageCodecBigDecimal: LogstageCodec[BigDecimal] = _.write(_)
   implicit final lazy val LogstageCodecBigInt: LogstageCodec[BigInt] = _.write(_)
+
   implicit final lazy val LogstageCodecThrowable: LogstageCodec[Throwable] = {
     (w, t) =>
       w.openMap()
-
-      w.write("type")
-      w.write(t.getClass.getName)
-
-      w.write("message")
-      val m = t.getMessage
-      if (m eq null) w.writeNull() else w.write(m)
-
-      w.write("stacktrace")
-      w.write(t.stackTrace)
-
+      w.writeMapElement("type", Some(t.getClass.getName))
+      w.writeMapElement("message", Option(t.getMessage))
+      w.writeMapElement("stacktrace", Some(t.stackTrace))
       w.closeMap()
   }
 
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageReprWriter.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageReprWriter.scala
new file mode 100644
index 000000000..65a09a2b9
--- /dev/null
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageReprWriter.scala
@@ -0,0 +1,117 @@
+package izumi.logstage.api.rendering
+
+import izumi.fundamentals.platform.language.Quirks.Discarder
+import izumi.fundamentals.platform.language.unused
+import izumi.logstage.api.rendering.LogstageReprWriter.Token
+
+class LogstageReprWriter(@unused colored: Boolean) extends ExtendedLogstageWriter[String] {
+  private val stack = scala.collection.mutable.Stack[Token]()
+
+  def translate(): String = {
+    val boundaries = scala.collection.mutable.Stack[Token]()
+
+    while (stack.nonEmpty) {
+      stack.pop() match {
+        case Token.Open(m) =>
+          val elements = scala.collection.mutable.ArrayBuffer[String]()
+
+          while (boundaries.head != Token.Close) {
+            boundaries.pop() match {
+              case Token.Value(v) =>
+                elements += v
+              case t =>
+                throw new RuntimeException(s"Unexpected token: $t; stack=$stack, bstack=$boundaries")
+            }
+          }
+
+          boundaries.pop().discard()
+
+          if (m) {
+            val pairs = elements.sliding(2, 2).map {
+              e =>
+                s"${e.head}: ${e.last}"
+            }
+            boundaries.push(Token.Value(pairs.mkString("{", "; ", "}")))
+          } else {
+            boundaries.push(Token.Value(elements.mkString("; ")))
+          }
+
+        case Token.Close =>
+          boundaries.push(Token.Close)
+        case v: Token.Value =>
+          boundaries.push(v)
+      }
+    }
+
+    boundaries.flatMap {
+      case _: Token.Struct =>
+        Seq.empty
+      case Token.Value(value) =>
+        Seq(value)
+    }.toList match {
+      case one :: Nil =>
+        one
+      case Nil =>
+        "<???>"
+      case shouldNotHappen =>
+        shouldNotHappen.mkString(", ")
+    }
+  }
+
+  override def openList(): Unit = stack.push(Token.Open(false))
+
+  override def closeList(): Unit = stack.push(Token.Close)
+
+  override def openMap(): Unit = stack.push(Token.Open(true))
+
+  override def closeMap(): Unit = stack.push(Token.Close)
+
+  override def nextListElementClose(): Unit = {}
+
+  override def nextMapElementClose(): Unit = {}
+
+  override def mapElementSplitter(): Unit = {}
+
+  override def nextListElementOpen(): Unit = {}
+
+  override def nextMapElementOpen(): Unit = {}
+
+  override def writeNull(): Unit = str("<null>")
+
+  override def write(a: Boolean): Unit = str(a)
+
+  override def write(a: Byte): Unit = str(a)
+
+  override def write(a: Short): Unit = str(a)
+
+  override def write(a: Char): Unit = str(a)
+
+  override def write(a: Int): Unit = str(a)
+
+  override def write(a: Long): Unit = str(a)
+
+  override def write(a: Float): Unit = str(a)
+
+  override def write(a: Double): Unit = str(a)
+
+  override def write(a: String): Unit = str(a)
+
+  override def write(a: BigDecimal): Unit = str(a)
+
+  override def write(a: BigInt): Unit = str(a)
+
+  @inline private[this] def str(a: Any): Unit = {
+    stack.push(Token.Value(a.toString))
+  }
+}
+
+object LogstageReprWriter {
+  sealed trait Token
+  object Token {
+    sealed trait Struct extends Token
+    final case class Value(value: String) extends Token
+    final case class Open(map: Boolean) extends Struct
+    case object Close extends Struct
+  }
+
+}
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriter.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriter.scala
index ac0688d77..a14bb26ff 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriter.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriter.scala
@@ -12,6 +12,19 @@ trait LogstageWriter extends LogstageScalarWriter {
   def mapElementSplitter(): Unit
 
   def writeNull(): Unit
+
+  final def writeMapElement(key: String, value: Option[String]): Unit = {
+    nextMapElementOpen()
+    write(key)
+    mapElementSplitter()
+    value match {
+      case Some(value) =>
+        write(value)
+      case None =>
+        writeNull()
+    }
+    nextMapElementClose()
+  }
 }
 
 trait LogstageScalarWriter {
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriterResult.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriterResult.scala
new file mode 100644
index 000000000..7ae3b3fef
--- /dev/null
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/LogstageWriterResult.scala
@@ -0,0 +1,5 @@
+package izumi.logstage.api.rendering
+
+trait LogstageWriterResult[T] {
+  def translate(): T
+}
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/StringRenderingPolicy.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/StringRenderingPolicy.scala
index 037745aee..24adc0539 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/StringRenderingPolicy.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/StringRenderingPolicy.scala
@@ -1,7 +1,6 @@
 package izumi.logstage.api.rendering
 
 import izumi.fundamentals.platform.basics.IzBoolean._
-import izumi.fundamentals.platform.exceptions.IzThrowable._
 import izumi.fundamentals.platform.jvm.IzJvm
 import izumi.fundamentals.platform.time.IzTimeSafe
 import izumi.logstage.DebugProperties
@@ -37,33 +36,7 @@ class StringRenderingPolicy(
   }
 
   override def render(entry: Log.Entry): String = {
-    val sb = new StringBuffer()
-
-    sb.append(renderer.render(entry, context))
-
-    if (options.withExceptions) {
-      sb.append(traceThrowable(entry))
-    }
-
-    sb.toString
-  }
-
-  def traceThrowable(entry: Log.Entry): String = {
-    entry.firstThrowable match {
-      case Some(t) =>
-        val builder = new StringBuilder
-        builder.append('\n')
-        if (context.colored) {
-          builder.append(Console.YELLOW)
-        }
-        builder.append(t.stackTrace)
-        if (context.colored) {
-          builder.append(Console.RESET)
-        }
-        builder.toString()
-      case None =>
-        ""
-    }
+    renderer.render(entry, context)
   }
 }
 
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/Extractor.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/Extractor.scala
index 16953d457..2ef5bfcd8 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/Extractor.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/Extractor.scala
@@ -69,7 +69,7 @@ object Extractor {
 
   class Message extends Extractor {
     override def render(entry: Log.Entry, context: RenderingOptions): LETree.TextNode = {
-      LETree.TextNode(LogFormat.Default.formatMessage(entry, context.colored).message)
+      LETree.TextNode(LogFormat.Default.formatMessage(entry, context).message)
     }
   }
 
@@ -79,7 +79,11 @@ object Extractor {
       val values = entry.context.customContext.values
 
       val out = if (values.nonEmpty) {
-        values.map(v => LogFormat.Default.formatKv(context.colored)(v.name, v.value)).mkString(", ")
+        values
+          .map {
+            v => LogFormat.Default.formatKv(context.colored)(v.name, v.codec, v.value)
+          }
+          .mkString(", ")
       } else {
         ""
       }
diff --git a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/LogFormat.scala b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/LogFormat.scala
index eb7d760d1..ea4393ee7 100644
--- a/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/LogFormat.scala
+++ b/logstage/logstage-core/src/main/scala/izumi/logstage/api/rendering/logunits/LogFormat.scala
@@ -3,27 +3,33 @@ package izumi.logstage.api.rendering.logunits
 import izumi.fundamentals.platform.exceptions.IzThrowable
 import izumi.logstage.api.Log
 import izumi.logstage.api.Log.LogArg
-import izumi.logstage.api.rendering.{RenderedMessage, RenderedParameter}
+import izumi.logstage.api.rendering.{LogstageCodec, RenderedMessage, RenderedParameter, RenderingOptions}
 
 import scala.collection.mutable
 import scala.collection.mutable.ArrayBuffer
-import scala.util.{Failure, Success, Try}
 
 trait LogFormat {
-  @inline def formatArg(arg: LogArg, withColors: Boolean): RenderedParameter
-  @inline def formatMessage(entry: Log.Entry, withColors: Boolean): RenderedMessage
+  @inline def formatMessage(entry: Log.Entry, options: RenderingOptions): RenderedMessage
 
-  @inline private[logunits] def formatKv(withColor: Boolean)(name: String, value: Any): String
+  @inline def formatArg(arg: LogArg, withColors: Boolean): RenderedParameter
+  @inline def formatKv(withColor: Boolean)(name: String, codec: Option[LogstageCodec[Any]], value: Any): String
 }
 
 object LogFormat {
 
   abstract class LogFormatImpl extends LogFormat {
     def formatArg(arg: LogArg, withColors: Boolean): RenderedParameter = {
-      RenderedParameter(arg, argToString(arg.value, withColors), normalizeName(arg.name))
+      RenderedParameter(arg, argToString(arg.codec, arg.value, withColors), normalizeName(arg.name))
     }
 
-    def formatMessage(entry: Log.Entry, withColors: Boolean): RenderedMessage = {
+    def formatKv(withColor: Boolean)(name: String, codec: Option[LogstageCodec[Any]], value: Any): String = {
+      val key = wrapped(withColor, Console.GREEN, name)
+      val v = argToString(codec, value, withColor)
+      s"$key=$v"
+    }
+
+    def formatMessage(entry: Log.Entry, options: RenderingOptions): RenderedMessage = {
+      val withColors = options.colored
       val templateBuilder = new StringBuilder()
       val messageBuilder = new StringBuilder()
 
@@ -43,13 +49,42 @@ object LogFormat {
       val unbalanced = entry.message.args.takeRight(entry.message.args.length - balanced.length)
       processUnbalanced(occurences, withColors, templateBuilder, messageBuilder, unbalancedArgs, unbalanced)
 
+      if (options.withExceptions) {
+        messageBuilder.append(traceThrowables(options, entry))
+      }
+
       RenderedMessage(entry, templateBuilder.toString(), messageBuilder.toString(), parameters.toSeq, unbalancedArgs.toSeq)
     }
 
-    @inline private[logunits] def formatKv(withColor: Boolean)(name: String, value: Any): String = {
-      val key = wrapped(withColor, Console.GREEN, name)
-      val v = argToString(value, withColor)
-      s"$key=$v"
+    def traceThrowables(options: RenderingOptions, entry: Log.Entry): String = {
+      import izumi.fundamentals.platform.exceptions.IzThrowable._
+
+      val throwables = entry.throwables
+      if (throwables.nonEmpty) {
+        throwables
+          .zipWithIndex
+          .map {
+            case (t, idx) =>
+              val builder = new StringBuilder
+              if (options.colored) {
+                builder.append(Console.YELLOW)
+                builder.append("💔 ")
+              }
+              if (throwables.size > 1) {
+                builder.append(s"Exception #$idx:\n")
+              }
+
+              // TODO: we may try to use codec here
+              builder.append(t.value.stackTrace)
+              if (options.colored) {
+                builder.append(Console.RESET)
+              }
+              builder.toString()
+          }
+          .mkString("\n", "\n", "")
+      } else {
+        ""
+      }
     }
 
     @inline private[this] def processUnbalanced(
@@ -112,13 +147,13 @@ object LogFormat {
           templateBuilder.append(handle(part))
 
           val maybeColoredRepr = if (withColors) {
-            argToString(uncoloredRepr.arg.value, withColors)
+            argToString(arg.codec, uncoloredRepr.arg.value, withColors)
           } else {
             uncoloredRepr.repr
           }
 
           if (!uncoloredRepr.arg.hiddenName) {
-            messageBuilder.append(formatKv(withColors)(visibleName, maybeColoredRepr))
+            messageBuilder.append(formatKvStrings(withColors, visibleName, maybeColoredRepr))
           } else {
             messageBuilder.append(maybeColoredRepr)
           }
@@ -139,20 +174,34 @@ object LogFormat {
       StringContext.processEscapes(part)
     }
 
-    @inline private[this] def argToString(argValue: Any, withColors: Boolean): String = {
+    @inline private[this] def formatKvStrings(withColor: Boolean, name: String, value: String): String = {
+      val key = wrapped(withColor, Console.GREEN, name)
+      val v = wrapped(withColor, Console.CYAN, value)
+      s"$key=$v"
+    }
+
+    @inline private[this] def argToString(codec: Option[LogstageCodec[Any]], argValue: Any, withColors: Boolean): String = {
       argValue match {
         case null =>
           wrapped(withColors, Console.YELLOW, "null")
 
         case e: Throwable =>
+          // TODO: we may try to use codec here
           wrapped(withColors, Console.YELLOW, e.toString)
 
         case _ =>
-          Try(toString(argValue)) match {
-            case Success(s) =>
-              wrapped(withColors, Console.CYAN, s)
-
-            case Failure(f) =>
+          try {
+            codec match {
+              case Some(codec) =>
+                val writer = codec.makeReprWriter(withColors)
+                codec.write(writer, argValue)
+                wrapped(withColors, Console.CYAN, writer.translate())
+              case None =>
+                wrapped(withColors, Console.CYAN, toString(argValue))
+
+            }
+          } catch {
+            case f: Throwable =>
               import IzThrowable._
               val message = s"[${argValue.getClass.getName}#toString failed]\n${f.stackTrace} "
               wrapped(withColors, Console.RED, message)
diff --git a/logstage/logstage-rendering-circe/src/main/scala/izumi/logstage/api/rendering/json/LogstageCirceRenderingPolicy.scala b/logstage/logstage-rendering-circe/src/main/scala/izumi/logstage/api/rendering/json/LogstageCirceRenderingPolicy.scala
index a93d1ccb4..ab18ae146 100644
--- a/logstage/logstage-rendering-circe/src/main/scala/izumi/logstage/api/rendering/json/LogstageCirceRenderingPolicy.scala
+++ b/logstage/logstage-rendering-circe/src/main/scala/izumi/logstage/api/rendering/json/LogstageCirceRenderingPolicy.scala
@@ -5,7 +5,7 @@ import io.circe.syntax._
 import izumi.logstage.api.Log
 import izumi.logstage.api.Log.LogArg
 import izumi.logstage.api.rendering.logunits.LogFormat
-import izumi.logstage.api.rendering.{LogstageCodec, RenderedMessage, RenderedParameter, RenderingPolicy}
+import izumi.logstage.api.rendering.{LogstageCodec, RenderedMessage, RenderedParameter, RenderingOptions, RenderingPolicy}
 
 import scala.collection.mutable
 import scala.runtime.RichInt
@@ -24,7 +24,7 @@ class LogstageCirceRenderingPolicy(
   override def render(entry: Log.Entry): String = {
     val result = mutable.ArrayBuffer[(String, Json)]()
 
-    val formatted = Format.formatMessage(entry, withColors = false)
+    val formatted = Format.formatMessage(entry, RenderingOptions(withExceptions = false, colored = false))
     val params = parametersToJson[RenderedParameter](
       formatted.parameters ++ formatted.unbalanced,
       _.normalizedName,


Test patch:
diff --git a/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala b/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
index 326c14cd3..ca87e2e1c 100644
--- a/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
+++ b/fundamentals/fundamentals-bio/src/test/scala/izumi/fundamentals/bio/test/BIOSyntaxTest.scala
@@ -148,7 +148,31 @@ class BIOSyntaxTest extends AnyWordSpec {
         (1, 2) <- F.pure((2, 1))
       } yield ()
     }
+    def y[F[+_, +_]: BIOError]: F[Any, Unit] = {
+      for {
+        (1, 2) <- F.pure((2, 1))
+      } yield ()
+    }
+    def z[F[+_, +_]: BIOError]: F[String, Unit] = {
+      for {
+        (1, 2) <- F.pure((2, 1)).widenError[String]
+      } yield ()
+    }
+    def xx[F[+_, +_]: BIOError]: F[Unit, Unit] = {
+      for {
+        (1, 2) <- F.pure((2, 1)).widenError[Unit]
+      } yield ()
+    }
+    def yy[F[+_, +_]: BIOError]: F[Option[Throwable], Unit] = {
+      for {
+        (1, 2) <- F.pure((2, 1)).widenError[Option[Throwable]]
+      } yield ()
+    }
     x[zio.IO]
+    y[zio.IO]
+    z[zio.IO]
+    xx[zio.IO]
+    yy[zio.IO]
   }
 
   "F / FR summoners examples" in {
diff --git a/logstage/logstage-core/src/test/scala/izumi/logstage/sink/ExampleService.scala b/logstage/logstage-core/src/test/scala/izumi/logstage/sink/ExampleService.scala
index 2155ade74..960c2131d 100644
--- a/logstage/logstage-core/src/test/scala/izumi/logstage/sink/ExampleService.scala
+++ b/logstage/logstage-core/src/test/scala/izumi/logstage/sink/ExampleService.scala
@@ -72,7 +72,7 @@ class ExampleService(logger: IzLogger) {
 
   private def testCornercases(): Unit = {
     val arg1 = 5
-    val nullarg = null
+    val nullarg: Integer = null
     val exception = makeException("Boom!")
     logger.warn("[Cornercase] non-interpolated expression: " + 1)
     logger.crit(s"[Cornercase] Anonymous expression: ${2 + 2 == 4}, another one: ${5 * arg1 == 25}")