Skip to content

Commit 8d69b5c

Browse files
authored
Add catching extension method, rename previous top-level catching to catchingNonFatal (#345)
1 parent 2c40b26 commit 8d69b5c

File tree

5 files changed

+55
-14
lines changed

5 files changed

+55
-14
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ val result1: (Int, String) = par(computation1, computation2)
4747

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

core/src/main/scala/ox/either.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import scala.compiletime.{error, summonFrom}
55
import scala.util.{NotGiven, boundary}
66
import scala.util.boundary.{Label, break}
77
import scala.util.control.NonFatal
8+
import scala.reflect.ClassTag
89

910
object either:
1011

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

@@ -143,4 +144,10 @@ object either:
143144
def orThrow: T = e match
144145
case Right(value) => value
145146
case Left(throwable) => throw throwable
147+
148+
extension [T](inline t: T)
149+
/** Catches `E` exceptions that occur when evaluating `t` and returns them as the left side of the returned `Either`. */
150+
inline def catching[E <: Throwable: ClassTag]: Either[E, T] =
151+
try Right(t)
152+
catch case e: E => Left(e)
146153
end either

core/src/test/scala/ox/EitherTest.scala

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package ox
33
import org.scalatest.exceptions.TestFailedException
44
import org.scalatest.flatspec.AnyFlatSpec
55
import org.scalatest.matchers.should.Matchers
6-
import ox.either.{fail, ok, orThrow}
6+
import ox.either.catching
7+
import ox.either.fail
8+
import ox.either.ok
9+
import ox.either.orThrow
710

811
import scala.util.boundary.Label
912

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

119-
it should "catch exceptions" in {
120-
either.catching(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
122+
it should "catch non fatal exceptions" in {
123+
either.catchingNonFatal(throw new RuntimeException("boom")).left.map(_.getMessage) shouldBe Left("boom")
121124
}
122125

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

126129
e shouldBe a[InterruptedException]
127130
}
128131

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

132-
either.catching(val1.ok()) shouldBe Left(ComparableException("oh no"))
135+
either.catchingNonFatal(val1.ok()) shouldBe Left(ComparableException("oh no"))
133136
}
134137

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

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

180+
"catching" should "catch given exceptions only" in {
181+
val e = new IllegalArgumentException("boom")
182+
(throw e).catching[IllegalArgumentException] shouldBe Left(e)
183+
}
184+
185+
it should "catch parent exceptions" in {
186+
val e = new IllegalArgumentException("boom")
187+
(throw e).catching[Exception] shouldBe Left(e)
188+
}
189+
190+
it should "not catch non-given exceptions" in {
191+
val e = new IllegalArgumentException("boom")
192+
intercept[IllegalArgumentException]((throw e).catching[IllegalStateException]) shouldBe e
193+
}
194+
195+
it should "return successful results as Right-values" in {
196+
10.catching[Exception] shouldBe Right(10)
197+
}
198+
177199
private transparent inline def receivesNoEitherNestingError(inline code: String): Unit =
178200
val errs = scala.compiletime.testing.typeCheckErrors(code)
179201
if !errs

doc/basics/error-handling.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,23 @@ val result: Either[String, Int] = either:
127127
if v1.ok() > 10 then 42 else "wrong".fail()
128128
```
129129

130-
Exception-throwing code can be converted to an `Either` using `catching`. Note that this only catches non-fatal
131-
exceptions!
130+
An exception-throwing expression can be converted to an `Either` using the `.catching[E]` extension method:
131+
132+
```scala mdoc:compile-only
133+
import ox.either.catching
134+
135+
val userInput: Boolean = ???
136+
val result: Either[IllegalArgumentException, Int] =
137+
(if userInput then 10 else throw new IllegalArgumentException("boom"))
138+
.catching[IllegalArgumentException]
139+
```
140+
141+
Arbitrary exception-throwing code can be converted to an `Either` using `catchingNonFatal`:
132142

133143
```scala mdoc:compile-only
134144
import ox.either
135145

136-
val result: Either[Throwable, String] = either.catching(throw new RuntimeException("boom"))
146+
val result: Either[Throwable, String] = either.catchingNonFatal(throw new RuntimeException("boom"))
137147
```
138148

139149
### Nested `either` blocks

doc/tour.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
```scala mdoc:invisible
44
import ox.*
55
import ox.either.ok
6+
import ox.either.catching
67
import ox.channels.*
78
import ox.flow.Flow
89
import ox.resilience.*
910
import ox.scheduling.*
1011
import scala.concurrent.duration.*
12+
import scala.concurrent.TimeoutException
1113
```
1214

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

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

0 commit comments

Comments
 (0)