diff --git a/README.md b/README.md
index 789215b..b984fbe 100644
--- a/README.md
+++ b/README.md
@@ -536,6 +536,28 @@ val usdAmount: Double = Raise.recoverable {
The strategy lets us define how to recover from a typed error in a dedicated place, leaving the happy logic in the main
block.
+### Tracing
+
+When a logical error is raised, the library will not print any information about the error by default. Sometimes, it's useful for debugging purposes to have a trace of the error. The library allows you to define a strategy called `TraceWith` that processes the error together with a stack trace that identifies where the error was raised.
+
+To enable tracing for a specific code block, wrap it inside the `trace` DSL and provide a `TraceWith` strategy:
+
+```scala 3
+import in.rcard.raise4s.Raise.{raise, traced}
+import in.rcard.raise4s.Strategies.TraceWith
+
+given TraceWith[String] = (trace: Traced) => {
+ trace.printStackTrace()
+}
+val lambda: Int raises String = traced {
+ raise("Oops!")
+}
+val actual: String | Int = Raise.run(lambda)
+actual shouldBe "Oops!"
+```
+
+The `Traced` exception the tracing engine uses contains the original typed error.
+
## Contributing
If you want to contribute to the project, please do it! Any help is welcome.
diff --git a/core/src/main/scala/in/rcard/raise4s/Raise.scala b/core/src/main/scala/in/rcard/raise4s/Raise.scala
index 0d3e10d..a898dd5 100644
--- a/core/src/main/scala/in/rcard/raise4s/Raise.scala
+++ b/core/src/main/scala/in/rcard/raise4s/Raise.scala
@@ -1,7 +1,7 @@
package in.rcard.raise4s
import in.rcard.raise4s
-import in.rcard.raise4s.Strategies.RecoverWith
+import in.rcard.raise4s.Strategies.{RecoverWith, TraceWith, Traced, TracedRaise}
import scala.annotation.targetName
import scala.util.Try
@@ -1768,4 +1768,44 @@ object Raise {
*/
inline def withDefault[Error, A](default: A)(inline block: Raise[Error] ?=> A): A =
Raise.recover(block)(error => default)
+
+ /** Add tracing to a block of code that can raise a logical error. In detail, the logical error is
+ * wrapped inside a [[Traced]] exception and processed by the [[TraceWith]] strategy instance.
+ * Please, be aware that adding tracing to an error can have a performance impact since a fat
+ * exception with a full stack trace is created.
+ *
+ *
Example
+ * {{{
+ * given TraceWith[String] = trace => {
+ * trace.printStackTrace()
+ * }
+ * val lambda: Int raises String = traced {
+ * raise("Oops!")
+ * }
+ * val actual: String | Int = Raise.run(lambda)
+ * actual shouldBe "Oops!"
+ * }}}
+ *
+ * @param block
+ * The block of code to execute that can raise an error
+ * @param tracing
+ * The strategy to process the traced error
+ * @tparam Error
+ * The type of the logical error that can be raised by the `block` lambda
+ * @tparam A
+ * The type of the result of the execution of `block` lambda
+ * @return
+ * The original block wrapped into a traced block
+ */
+ inline def traced[Error, A](
+ inline block: Raise[Error] ?=> A
+ )(using inline tracing: TraceWith[Error]): Raise[Error] ?=> A = {
+ try {
+ given tracedRaise: Raise[Error] = new TracedRaise
+ block
+ } catch
+ case traced: Traced[Error] =>
+ tracing.trace(traced)
+ Raise.raise(traced.original)
+ }
}
diff --git a/core/src/main/scala/in/rcard/raise4s/Strategies.scala b/core/src/main/scala/in/rcard/raise4s/Strategies.scala
index 36eecb4..2586ce5 100644
--- a/core/src/main/scala/in/rcard/raise4s/Strategies.scala
+++ b/core/src/main/scala/in/rcard/raise4s/Strategies.scala
@@ -61,12 +61,53 @@ object Strategies {
* actual should be(43)
* }}}
*
- * @tparam Error The type of the error to recover from
- * @tparam A The type of the value to return if the recovery is successful
- *
- * @see [[Raise.recoverable]]
+ * @tparam Error
+ * The type of the error to recover from
+ * @tparam A
+ * The type of the value to return if the recovery is successful
+ *
+ * @see
+ * [[Raise.recoverable]]
*/
trait RecoverWith[Error, A] {
def recover(error: Error): A
}
+
+ /** Implement the `raise` method to throw a [[Traced]] exception with the error to trace instead
+ * of a [[Raised]] exception.
+ */
+ private[raise4s] class TracedRaise extends Raise[Any]:
+ def raise(e: Any): Nothing = throw Traced(e)
+
+ /** The exception that wraps the original error in case of tracing. The difference with the [[Raised]] exception is that
+ * the exception contains a full stack trace.
+ * @param original
+ * The original error to trace
+ * @tparam Error
+ * The type of the error to trace
+ */
+ case class Traced[Error](original: Error) extends Exception
+
+ /** A strategy that allows to trace an error and return it. The [[trace]] method represent the
+ * behavior to trace the error. As a strategy, it should be used as a `given` instance. Use the
+ * type class instance with the [[Raise.traced]] DSL.
+ *
+ * Example
+ * {{{
+ * given TraceWith[String] = trace => {
+ * trace.printStackTrace()
+ * }
+ * val lambda: Int raises String = traced {
+ * raise("Oops!")
+ * }
+ * val actual: String | Int = Raise.run(lambda)
+ * actual shouldBe "Oops!"
+ * }}}
+ *
+ * @tparam Error
+ * The type of the error to trace
+ */
+ trait TraceWith[Error] {
+ def trace(traced: Traced[Error]): Unit
+ }
}
diff --git a/core/src/test/scala/in/rcard/raise4s/StrategiesSpec.scala b/core/src/test/scala/in/rcard/raise4s/StrategiesSpec.scala
index bfde237..ee1cf34 100644
--- a/core/src/test/scala/in/rcard/raise4s/StrategiesSpec.scala
+++ b/core/src/test/scala/in/rcard/raise4s/StrategiesSpec.scala
@@ -1,7 +1,7 @@
package in.rcard.raise4s
-import in.rcard.raise4s.Raise.raise
-import in.rcard.raise4s.Strategies.{MapError, anyRaised}
+import in.rcard.raise4s.Raise.{raise, traced}
+import in.rcard.raise4s.Strategies.{MapError, TraceWith, anyRaised}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
@@ -39,4 +39,38 @@ class StrategiesSpec extends AnyFlatSpec with Matchers {
val result: Int | String = Raise.run(finalLambda)
result shouldBe "Hello"
}
+
+ "TraceWith" should "allow defining a strategy that trace the error and return it" in {
+ val queue = collection.mutable.ListBuffer.empty[String]
+ given TraceWith[String] = trace => {
+ queue += trace.original
+ trace.printStackTrace()
+ }
+
+ val lambda: Int raises String = traced {
+ raise("Oops!")
+ }
+
+ val actual: String | Int = Raise.run(lambda)
+
+ actual shouldBe "Oops!"
+ queue should contain("Oops!")
+ }
+
+ it should "return the happy path value if no error is raised" in {
+ val queue = collection.mutable.ListBuffer.empty[String]
+ given TraceWith[String] = trace => {
+ queue += trace.original
+ trace.printStackTrace()
+ }
+
+ val lambda: Int raises String = traced {
+ 42
+ }
+
+ val actual: Int | String = Raise.run(lambda)
+
+ actual shouldBe 42
+ queue shouldBe empty
+ }
}