Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ val result1: (Int, String) = par(computation1, computation2)

```scala mdoc:compile-only
def computation3: Int = { sleep(2.seconds); 1 }
val result2: Either[Throwable, Int] = either.catching(timeout(1.second)(computation3))
val result2: Either[TimeoutException, Int] = timeout(1.second)(computation3).catching[TimeoutException]
// `timeout` only completes once the loosing branch is interrupted & done
```

Expand Down
9 changes: 8 additions & 1 deletion core/src/main/scala/ox/either.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import scala.compiletime.{error, summonFrom}
import scala.util.{NotGiven, boundary}
import scala.util.boundary.{Label, break}
import scala.util.control.NonFatal
import scala.reflect.ClassTag

object either:

/** Catches non-fatal exceptions that occur when evaluating `t` and returns them as the left side of the returned `Either`. */
inline def catching[T](inline t: Label[Either[Throwable, T]] ?=> T): Either[Throwable, T] =
inline def catchingNonFatal[T](inline t: Label[Either[Throwable, T]] ?=> T): Either[Throwable, T] =
try boundary(Right(t))
catch case NonFatal(e) => Left(e)

Expand Down Expand Up @@ -143,4 +144,10 @@ object either:
def orThrow: T = e match
case Right(value) => value
case Left(throwable) => throw throwable

extension [T](inline t: T)
/** Catches `E` exceptions that occur when evaluating `t` and returns them as the left side of the returned `Either`. */
inline def catching[E <: Throwable: ClassTag]: Either[E, T] =
try Right(t)
catch case e: E => Left(e)
end either
38 changes: 30 additions & 8 deletions core/src/test/scala/ox/EitherTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package ox
import org.scalatest.exceptions.TestFailedException
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import ox.either.{fail, ok, orThrow}
import ox.either.catching
import ox.either.fail
import ox.either.ok
import ox.either.orThrow

import scala.util.boundary.Label

Expand Down Expand Up @@ -116,24 +119,24 @@ class EitherTest extends AnyFlatSpec with Matchers:
e.getMessage should include("The enclosing `either` call uses a different error type.")
}

it should "catch exceptions" in {
either.catching(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
it should "catch non fatal exceptions" in {
either.catchingNonFatal(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
}

it should "not catch fatal exceptions" in {
val e = intercept[InterruptedException](either.catching(throw new InterruptedException()))
val e = intercept[InterruptedException](either.catchingNonFatal(throw new InterruptedException()))

e shouldBe a[InterruptedException]
}

it should "provide an either scope when catching" in {
it should "provide an either scope when catching non fatal exceptions" in {
val val1: Either[Throwable, Int] = Left(ComparableException("oh no"))

either.catching(val1.ok()) shouldBe Left(ComparableException("oh no"))
either.catchingNonFatal(val1.ok()) shouldBe Left(ComparableException("oh no"))
}

it should "report a proper compilation error when wrong error type is used for ok() in catching block" in {
val e = intercept[TestFailedException](assertCompiles("""either.catching(fail1.ok())"""))
it should "report a proper compilation error when wrong error type is used for ok() in catchingNonFatal block" in {
val e = intercept[TestFailedException](assertCompiles("""either.catchingNonFatal(fail1.ok())"""))

e.getMessage should include("The enclosing `either` call uses a different error type.")
}
Expand Down Expand Up @@ -174,6 +177,25 @@ class EitherTest extends AnyFlatSpec with Matchers:
intercept[RuntimeException](v.orThrow).getMessage shouldBe "boom!"
}

"catching" should "catch given exceptions only" in {
val e = new IllegalArgumentException("boom")
(throw e).catching[IllegalArgumentException] shouldBe Left(e)
}

it should "catch parent exceptions" in {
val e = new IllegalArgumentException("boom")
(throw e).catching[Exception] shouldBe Left(e)
}

it should "not catch non-given exceptions" in {
val e = new IllegalArgumentException("boom")
intercept[IllegalArgumentException]((throw e).catching[IllegalStateException]) shouldBe e
}

it should "return successful results as Right-values" in {
10.catching[Exception] shouldBe Right(10)
}

private transparent inline def receivesNoEitherNestingError(inline code: String): Unit =
val errs = scala.compiletime.testing.typeCheckErrors(code)
if !errs
Expand Down
16 changes: 13 additions & 3 deletions doc/basics/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,23 @@ val result: Either[String, Int] = either:
if v1.ok() > 10 then 42 else "wrong".fail()
```

Exception-throwing code can be converted to an `Either` using `catching`. Note that this only catches non-fatal
exceptions!
An exception-throwing expression can be converted to an `Either` using the `.catching[E]` extension method:

```scala mdoc:compile-only
import ox.either.catching

val userInput: Boolean = ???
val result: Either[IllegalArgumentException, Int] =
(if userInput then 10 else throw new IllegalArgumentException("boom"))
.catching[IllegalArgumentException]
```

Arbitrary exception-throwing code can be converted to an `Either` using `catchingNonFatal`:

```scala mdoc:compile-only
import ox.either

val result: Either[Throwable, String] = either.catching(throw new RuntimeException("boom"))
val result: Either[Throwable, String] = either.catchingNonFatal(throw new RuntimeException("boom"))
```

### Nested `either` blocks
Expand Down
4 changes: 3 additions & 1 deletion doc/tour.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
```scala mdoc:invisible
import ox.*
import ox.either.ok
import ox.either.catching
import ox.channels.*
import ox.flow.Flow
import ox.resilience.*
import ox.scheduling.*
import scala.concurrent.duration.*
import scala.concurrent.TimeoutException
```

Run two computations [in parallel](high-level-concurrency/par.md):
Expand All @@ -23,7 +25,7 @@ val result1: (Int, String) = par(computation1, computation2)

```scala mdoc:compile-only
def computation3: Int = { sleep(2.seconds); 1 }
val result2: Either[Throwable, Int] = either.catching(timeout(1.second)(computation3))
val result2: Either[TimeoutException, Int] = timeout(1.second)(computation3).catching[TimeoutException]
// `timeout` only completes once the loosing branch is interrupted & done
```

Expand Down