Skip to content

Add .tapFailure to TryOps #609

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 25, 2025
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
29 changes: 25 additions & 4 deletions core/src/main/scala/com/avsystem/commons/SharedExtensions.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.avsystem.commons

import com.avsystem.commons.concurrent.RunNowEC
import com.avsystem.commons.misc._
import com.avsystem.commons.misc.*

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

trait SharedExtensions {

import com.avsystem.commons.SharedExtensionsUtils._
import com.avsystem.commons.SharedExtensionsUtils.*

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

Expand Down Expand Up @@ -461,6 +461,27 @@ object SharedExtensionsUtils extends SharedExtensions {
*/
def toOptArg: OptArg[A] =
if (tr.isFailure) OptArg.Empty else OptArg(tr.get)

/**
* Apply side-effect only if Try is a failure. The provided `action` function will be called with the
* throwable from the failure case, allowing you to perform operations like logging or error handling.
*
* Non-fatal exceptions thrown by the `action` function are caught and ignored, ensuring that this method
* always returns the original Try instance regardless of what happens in the action.
*
* Don't use .failed projection, because it unnecessarily creates Exception in case of Success,
* which is an expensive operation.
*/
def tapFailure(action: Throwable => Unit): Try[A] = tr match {
case Success(_) => tr
case Failure(throwable) =>
try action(throwable)
catch {
case NonFatal(_) => // ignore non-fatal exceptions thrown by the action
}
tr

}
}

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

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

import PartialFunctionOps._
import PartialFunctionOps.*

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

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

import MapOps._
import MapOps.*

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.avsystem.commons.misc

import com.avsystem.commons.CommonAliases.*
import com.avsystem.commons.SharedExtensions.*
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

import com.avsystem.commons.SharedExtensions._
import com.avsystem.commons.CommonAliases._

class SharedExtensionsTest extends AnyFunSuite with Matchers {
test("mkMap") {
List.range(0, 3).mkMap(identity, _.toString) shouldEqual
Expand Down Expand Up @@ -81,7 +80,7 @@ class SharedExtensionsTest extends AnyFunSuite with Matchers {
}

test("Future.transformWith") {
import com.avsystem.commons.concurrent.RunNowEC.Implicits._
import com.avsystem.commons.concurrent.RunNowEC.Implicits.*
val ex = new Exception
assert(Future.successful(42).transformWith(t => Future.successful(t.get - 1)).value.contains(Success(41)))
assert(Future.successful(42).transformWith(_ => Future.failed(ex)).value.contains(Failure(ex)))
Expand Down Expand Up @@ -206,4 +205,46 @@ class SharedExtensionsTest extends AnyFunSuite with Matchers {
| abc
| abc""".stripMargin)
}

test("Try.tapFailure - Success case") {
var actionCalled = false
val successTry = Success(42)
val result = successTry.tapFailure(_ => actionCalled = true)

assert(!actionCalled, "Action should not be called for Success")
assert(result === successTry, "Original Success should be returned")
}

test("Try.tapFailure - Failure case") {
var capturedThrowable: Throwable = null
val exception = new RuntimeException("test exception")
val failureTry = Failure(exception)

val result = failureTry.tapFailure(t => capturedThrowable = t)

assert(capturedThrowable === exception, "Action should be called with the exception")
assert(result === failureTry, "Original Failure should be returned")
}

test("Try.tapFailure - Exception in action") {
val originalException = new RuntimeException("original exception")
val actionException = new RuntimeException("action exception")
val failureTry = Failure(originalException)

val result = failureTry.tapFailure(_ => throw actionException)

assert(result === failureTry, "Original Failure should be returned even if action throws")
}

test("Try.tapFailure - Fatal exception in action") {
val originalException = new RuntimeException("original exception")
val fatalException = new OutOfMemoryError("fatal exception")
val failureTry = Failure(originalException)

val thrown = intercept[OutOfMemoryError] {
failureTry.tapFailure(_ => throw fatalException)
}

assert(thrown === fatalException, "Fatal exception should propagate out of tapFailure")
}
}