Skip to content

Commit d02b9c7

Browse files
authored
Merge pull request #609 from AVSystem/try-tapFailure
Add .tapFailure to TryOps
2 parents b5221cb + 036bb0d commit d02b9c7

File tree

2 files changed

+70
-8
lines changed

2 files changed

+70
-8
lines changed

core/src/main/scala/com/avsystem/commons/SharedExtensions.scala

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.avsystem.commons
22

33
import com.avsystem.commons.concurrent.RunNowEC
4-
import com.avsystem.commons.misc._
4+
import com.avsystem.commons.misc.*
55

66
import scala.annotation.{nowarn, tailrec}
77
import scala.collection.{AbstractIterator, BuildFrom, Factory, mutable}
88

99
trait SharedExtensions {
1010

11-
import com.avsystem.commons.SharedExtensionsUtils._
11+
import com.avsystem.commons.SharedExtensionsUtils.*
1212

1313
implicit def universalOps[A](a: A): UniversalOps[A] = new UniversalOps(a)
1414

@@ -461,6 +461,27 @@ object SharedExtensionsUtils extends SharedExtensions {
461461
*/
462462
def toOptArg: OptArg[A] =
463463
if (tr.isFailure) OptArg.Empty else OptArg(tr.get)
464+
465+
/**
466+
* Apply side-effect only if Try is a failure. The provided `action` function will be called with the
467+
* throwable from the failure case, allowing you to perform operations like logging or error handling.
468+
*
469+
* Non-fatal exceptions thrown by the `action` function are caught and ignored, ensuring that this method
470+
* always returns the original Try instance regardless of what happens in the action.
471+
*
472+
* Don't use .failed projection, because it unnecessarily creates Exception in case of Success,
473+
* which is an expensive operation.
474+
*/
475+
def tapFailure(action: Throwable => Unit): Try[A] = tr match {
476+
case Success(_) => tr
477+
case Failure(throwable) =>
478+
try action(throwable)
479+
catch {
480+
case NonFatal(_) => // ignore non-fatal exceptions thrown by the action
481+
}
482+
tr
483+
484+
}
464485
}
465486

466487
class LazyTryOps[A](private val tr: () => Try[A]) extends AnyVal {
@@ -502,7 +523,7 @@ object SharedExtensionsUtils extends SharedExtensions {
502523

503524
class PartialFunctionOps[A, B](private val pf: PartialFunction[A, B]) extends AnyVal {
504525

505-
import PartialFunctionOps._
526+
import PartialFunctionOps.*
506527

507528
/**
508529
* The same thing as `orElse` but with arguments flipped.
@@ -638,7 +659,7 @@ object SharedExtensionsUtils extends SharedExtensions {
638659

639660
class MapOps[M[X, Y] <: BMap[X, Y], K, V](private val map: M[K, V]) extends AnyVal {
640661

641-
import MapOps._
662+
import MapOps.*
642663

643664
def getOpt(key: K): Opt[V] = map.get(key).toOpt
644665

core/src/test/scala/com/avsystem/commons/misc/SharedExtensionsTest.scala

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package com.avsystem.commons.misc
22

3+
import com.avsystem.commons.CommonAliases.*
4+
import com.avsystem.commons.SharedExtensions.*
35
import org.scalatest.funsuite.AnyFunSuite
46
import org.scalatest.matchers.should.Matchers
57

6-
import com.avsystem.commons.SharedExtensions._
7-
import com.avsystem.commons.CommonAliases._
8-
98
class SharedExtensionsTest extends AnyFunSuite with Matchers {
109
test("mkMap") {
1110
List.range(0, 3).mkMap(identity, _.toString) shouldEqual
@@ -81,7 +80,7 @@ class SharedExtensionsTest extends AnyFunSuite with Matchers {
8180
}
8281

8382
test("Future.transformWith") {
84-
import com.avsystem.commons.concurrent.RunNowEC.Implicits._
83+
import com.avsystem.commons.concurrent.RunNowEC.Implicits.*
8584
val ex = new Exception
8685
assert(Future.successful(42).transformWith(t => Future.successful(t.get - 1)).value.contains(Success(41)))
8786
assert(Future.successful(42).transformWith(_ => Future.failed(ex)).value.contains(Failure(ex)))
@@ -206,4 +205,46 @@ class SharedExtensionsTest extends AnyFunSuite with Matchers {
206205
| abc
207206
| abc""".stripMargin)
208207
}
208+
209+
test("Try.tapFailure - Success case") {
210+
var actionCalled = false
211+
val successTry = Success(42)
212+
val result = successTry.tapFailure(_ => actionCalled = true)
213+
214+
assert(!actionCalled, "Action should not be called for Success")
215+
assert(result === successTry, "Original Success should be returned")
216+
}
217+
218+
test("Try.tapFailure - Failure case") {
219+
var capturedThrowable: Throwable = null
220+
val exception = new RuntimeException("test exception")
221+
val failureTry = Failure(exception)
222+
223+
val result = failureTry.tapFailure(t => capturedThrowable = t)
224+
225+
assert(capturedThrowable === exception, "Action should be called with the exception")
226+
assert(result === failureTry, "Original Failure should be returned")
227+
}
228+
229+
test("Try.tapFailure - Exception in action") {
230+
val originalException = new RuntimeException("original exception")
231+
val actionException = new RuntimeException("action exception")
232+
val failureTry = Failure(originalException)
233+
234+
val result = failureTry.tapFailure(_ => throw actionException)
235+
236+
assert(result === failureTry, "Original Failure should be returned even if action throws")
237+
}
238+
239+
test("Try.tapFailure - Fatal exception in action") {
240+
val originalException = new RuntimeException("original exception")
241+
val fatalException = new OutOfMemoryError("fatal exception")
242+
val failureTry = Failure(originalException)
243+
244+
val thrown = intercept[OutOfMemoryError] {
245+
failureTry.tapFailure(_ => throw fatalException)
246+
}
247+
248+
assert(thrown === fatalException, "Fatal exception should propagate out of tapFailure")
249+
}
209250
}

0 commit comments

Comments
 (0)