Error Handling
One advantage of using Mutiny with Vert.x is that errors propagate through the reactive pipeline automatically.
With the callback or Future-based API you must manually check for failures at each step.
With Mutiny, failures flow downstream until something handles them, so you focus on the success path and deal with errors once:
// Errors propagate automatically
client.request(HttpMethod.GET, "/data")
.onItem().transformToUni(HttpClientRequest::send)
.onItem().transformToUni(HttpClientResponse::body)
.subscribe().with(
body -> System.out.println("Got: " + body),
failure -> System.err.println("Failed: " + failure.getMessage())
);
If any step in the pipeline fails, the remaining onItem() operators are skipped and the failure handler receives the error. With vanilla Vert.x, compose() chains behave similarly — a failure in any stage short-circuits to the onFailure handler — but the Mutiny API makes the distinction between item and failure handling more explicit through its operator vocabulary.
Basic failure handling
The simplest way to handle errors is to provide both a success and failure callback to subscribe().with():
If you omit the failure callback, unhandled failures are reported to Mutiny's global failure handler.
Recovery
You can recover from failures inline rather than letting them propagate to the subscriber.
Recover with a fallback item
Recover with another Uni
When recovery itself requires an asynchronous operation, use recoverWithUni:
Recover from specific exceptions
Both recovery methods accept a predicate or exception class to recover selectively:
With Mutiny, selective recovery is declarative — you chain .onFailure(ExceptionType.class) for each case. With vanilla Vert.x, you use a single recover() block and check types with instanceof.
Retries
Mutiny provides built-in retry support:
This retries the upstream operation up to 3 times on any failure.
With vanilla Vert.x, retries require a recursive method that calls itself from within a recover() block.
For production use, add exponential backoff to avoid overwhelming a failing service:
Future<String> fetchWithBackoff(int retries, long delayMs) {
return doFetch()
.recover(err -> {
if (retries > 0) {
Promise<String> promise = Promise.promise();
long nextDelay = Math.min(delayMs * 2, 5000);
vertx.setTimer(delayMs, id ->
fetchWithBackoff(retries - 1, nextDelay)
.onComplete(promise));
return promise.future();
}
return Future.failedFuture(err);
});
}
Future<String> result = fetchWithBackoff(5, 100);
This retries up to 5 times with an initial delay of 100ms that increases exponentially up to a maximum of 5 seconds between attempts.
With vanilla Vert.x, exponential backoff requires managing a Promise, a timer via vertx.setTimer(), and manual delay calculation — all of which Mutiny handles as a single operator chain.
Failure transformation
You can transform a failure into a different exception type, which is useful for wrapping low-level errors into domain-specific ones:
Vert.x expectations with Mutiny
Vert.x provides an Expectation interface that works as a predicate on resolved Future values.
Pre-defined expectations such as HttpResponseExpectation.status(200) are convenient for validating HTTP responses.
Since the Mutiny bindings convert Future-returning methods into Uni-returning methods, you cannot use expectations directly.
Instead, the io.smallrye.mutiny.vertx.core.Expectations helper class adapts them for use with Uni.plug().
Simple expectation
If the value does not satisfy the expectation, the Uni fails (or the Future fails in vanilla Vert.x).
For example, passing 42 instead of 15 would produce a failure with the message: Unexpected result: 42.
HTTP response expectations
Vert.x ships pre-defined expectations for HTTP responses, such as status() and contentType().
These expectations are typed against core Vert.x types (e.g., io.vertx.core.http.HttpResponseHead), not against the Mutiny shim types (e.g., io.vertx.mutiny.core.http.HttpResponseHead).
To bridge this gap, the expectation helper accepts an extractor function as its first argument.
Passing HttpClientResponse::getDelegate unwraps the Mutiny shim so the expectation receives the underlying Vert.x type:
import static io.smallrye.mutiny.vertx.core.Expectations.expectation;
import static io.vertx.core.http.HttpResponseExpectation.contentType;
import static io.vertx.core.http.HttpResponseExpectation.status;
vertx.createHttpClient()
.request(HttpMethod.GET, port, "localhost", "/")
.onItem().transformToUni(HttpClientRequest::send)
.plug(expectation(HttpClientResponse::getDelegate, status(200).and(contentType("text/plain"))))
.onItem().transformToUni(HttpClientResponse::body)
.await().atMost(Duration.ofSeconds(5));
import static io.vertx.core.http.HttpResponseExpectation.contentType;
import static io.vertx.core.http.HttpResponseExpectation.status;
vertx.createHttpClient()
.request(HttpMethod.GET, port, "localhost", "/")
.compose(HttpClientRequest::send)
.expecting(status(200).and(contentType("text/plain")))
.compose(HttpClientResponse::body)
.toCompletionStage().toCompletableFuture()
.get(5, TimeUnit.SECONDS);
In this example:
HttpClientResponse::getDelegateextracts the core Vert.x response from the Mutiny shim. With vanilla Vert.x, no unwrapping is needed since you work directly with the core types.status(200).and(contentType("text/plain"))combines two expectations: the response must have status 200 and content typetext/plain.- If either expectation fails, the
Unipipeline (or theFuturechain) fails with a descriptive error.
You can combine any number of expectations using .and().