I'm using Spring Framework with the Vavr library to handle exceptions using the Try
class.
While Try.of()
is well supported and integrates well with @Transactional
, I've noticed that using mapTry
/map
(map
is a shortcut for mapTry
) after Try.of()
doesn't propagate exceptions to onFailure
and instead throws exceptions with proxy class.
It will be nice to have this enhancement if it's possible so we can get exceptions on failure if something happens on mapTry
.
Steps to Reproduce:
- Create a Spring Boot application and include Vavr as a dependency.
- Use
@Transactional
annotation on a service method. - Use
Try.of()
andmapTry
in the method body.
@Transactional
public void myMethod() {
Try.of(() -> {
// Do something
})
.mapTry(value -> {
// Do something that throws an exception
throw new Exception("Error");
})
.onFailure(error -> {
// This block is not executed when an exception is thrown
});
}
Expected Behavior:
When an exception is thrown inside mapTry
, it should be caught by the onFailure
block.
Actual Behavior:
The exception is not caught by onFailure
.
Comment From: jhoeller
I'm not sure what we could do differently here. Is our internally registered onFailure
block for transaction processing somehow interfering with your declared one there? Beyond that, it's all standard Vavr semantics.
Comment From: Lunatix01
@jhoeller sorry if I confused you, what I mean here is when we use Transactional annotation in an example like this:
if findById
fails for any reason transactional doesn't return the error, onFailure catches the error, but in mapTry if this save fails for a reason transactional annotation throws an exception.
https://github.com/spring-projects/spring-framework/issues/20361 We have this issue that transactional supports Try its been implemented a long time ago, and I was wondering if it's possible to do with .mapTry also if it's possible.
public Try<Void> savePhoneNumber(PhoneNumberParams params) {
return Try.of(() ->
userRepository.findById(params.getUserId()).orElseThrow())
.mapTry(userEntity -> {
userEntity.setPhoneNumber(params.getPhoneNumber());
return userRepository.save(userEntity);
})
.<Void>map(ignored -> null)
.onFailure(ex -> log.error("phone number cannot be saved.", ex));
}
I hope I make sense here. and thanks for your time
Comment From: jhoeller
I see what you are trying to accomplish there. It's just that Spring is not really involved in that call arrangement up until the resulting Try
instance is returned from savePhoneNumber
, at which point we are going to add our internal onFailure
block for rollback handling. Not sure why the mapTry
block leads to an exception rather than an onFailure
invocation then. Shouldn't this be transparent for calling code that adds a further onFailure
block, being decoupled from how the original Try
instance has been composed and whether mapTry
has been involved there?
Comment From: Lunatix01
for your question yes, I also expected the behavior to be transparent. My assumption was that any onFailure
block I add should catch exceptions irrespective of how the Try
instance has been composed, including when using methods like mapTry
.
I'm curious that onFailure
block doesn't seem to catch exceptions raised within the mapTry
. I was wondering if Spring's transactional handling could be adjusted to account for this, or if this is something that needs to be addressed in conjunction with Vavr
's own functionality. Thank you for your time again.
Comment From: jhoeller
I'm afraid I cannot reproduce this, several variations of your second example work for me both in terms of Spring's rollback handling and with a custom onFailure
block always executing.
FWIW your first example does not actually return the Try
instance, so Spring's rollback handling won't kick in at all.
Comment From: Lunatix01
Sorry for the late reply sir, I created a POC repo, you can check it if it's possible, basically I run a postgres instance in docker, and in demo.sql
you can run those in the Postgres, so I made slug
column to be not more than 3 characters so I can make @Transactional
get failed by giving more than 3 characters, it throws DataIntegrityViolationException
.map
cant throw the exception to onFailure
This makes the application stop working and here is the piece of code in UserService
@Transactional
public void test() {
Try.of(() -> userRepository.findByName("aland").orElseThrow())
.map(userEntity -> {
userEntity.setSlug("aland");
return userRepository.save(userEntity);
})
.<Void>map(ignored -> null)
.onFailure(ex -> log.error("slug cannot be saved.", ex));
}
So I'm not sure here if I miss the concept of @Transactional
and the way it works with vavr
or what.
And thanks yes you are abs right, It was my bad I just wanted to show the skeleton of the error instead of a real example :)
Comment From: jhoeller
In the code example above, Spring is not actually involved. All that Spring does is to detect a returned Try
instance in order to add a transaction-driven onFailure
block that marks the transaction as rollback-only when a Vavr failure is present. With a void
method as above, there is nothing we can intercept there, it all happens within the method body.
So as long as you can structure your code to return a Try
instance from your @Transactional
method, and as long as that Try
instance exposes a failure that we can react to, you may rely on Spring's automatic rollback management. I'm afraid there isn't anything more we can do about this.
Comment From: Lunatix01
@jhoeller Thank you i understand now