how to handle errors in RxJava?

Viewed 32

It may sound funny or maybe novice, but I don't understand what it means that something "failed" in Reactive Programming. Do you mean a null? An empty object? someone could please give me examples.

Some of the scenarios I wish to realize are:

Assuming I send a query parameter and get either a listing with values or an empty listing; and lastly, I do not send the query parameter.

  • If an empty listing is issued I want to return an exception and a 404 status code.

  • If they do not send the query parameter I want to return an exception and some status code.

And of course, if a list with values is found, return it. Will it be possible to make these cases in a single method? how do I do it?

1 Answers

First, a reactor operator can ends in different ways:

  1. Completes successfully after emitting one (Mono) or more (Flux) value(s)
  2. Completes empty: The pipeline sends completion signal, but no value has been emitted
  3. Completes in error: somewhere in the pipeline, an error happened. By default, as in imperative code, it stops the chain of operations, and is propagated.
  4. Cancelled: the pipeline might be interrupted by a manual action or a system shutdown. It then ends in error (a special kind of error, but an error nonetheless)

Secondly, reactive-stream, whatever the implementation (RxJava or Reactor) does not accept null values. It means that trying to produce a null value in/from a reactive stream will either cause an error or an undefined behavior. This is stated in reactive-stream specification, rule 2.13:

Calling [...] onNext [...] MUST return normally except when any provided parameter is null in which case it MUST throw a java.lang.NullPointerException to the caller

Let's try to produce some simple examples first.

This program shows the possible ways a pipeline can complete:

// Reactive streams does not accept null values:
try {
    Mono.just(null);
} catch (NullPointerException e) {
    System.out.println("NULL VALUE NOT ACCEPTED !");
}

// Mono/Flux operations stop if an error occurs internally, and send it downstream
try {
    Mono.just("Something")
        .map(it -> { throw new IllegalStateException("Bouh !"); })
        .block();
} catch (IllegalStateException e) {
    System.out.println("Error propagated: "+e.getMessage());
}

// A mono or a flux can end "empty". It means that no value or error happened.
// The operation just finished without any result
var result = Mono.just("Hello !")
        .filter(it -> !it.endsWith("!"))
        // Materialize allow to receive the type of signal produced by the pipeline (next value, error, completion, etc.)
        .materialize()
        .block();
System.out.println("Input value has been filtered out. No 'next' value " +
        "received, just 'completion' signal:" + result.getType());

Its output:

NULL VALUE NOT ACCEPTED !
Error propagated: Bouh !
Input value has been filtered out. No 'next' value received, just 'completion' signal:onNext

Then, let's look at a program that intercept empty pipelines and errors, and handle them gracefully:

// Errors can be intercepted and replaced by a value:
var result = Mono.error(new IllegalStateException("No !"))
        .onErrorResume(err -> Mono.just("Override error: Hello again !"))
        .block();
System.out.println(result);

// Empty pipelines can also be replaced by another one that produce a value:
result = Mono.just("Hello !")
        .filter(it -> !it.endsWith("!"))
        .switchIfEmpty(Mono.just("Override empty: Hello again !"))
        .block();

System.out.println(result);

It produces:

Override error: Hello again !
Override empty: Hello again !

With all this tools, we can solve the problem you describe with your query.

Let's mimic it:

public static Flux<String> processRequest(String queryParam) {
    if (queryParam == null || queryParam.isEmpty()) return Flux.error(new IllegalArgumentException("Bad request"));
    return Mono.just(queryParam)
               .flatMapMany(param -> Flux.fromArray(param.split("_")))
               .switchIfEmpty(Mono.error(new IllegalStateException("No data")));
}

public static void main(String[] args) {
    
    String[] inputs = { null, "hello_world", "___" };
    
    for (var input : inputs) {
        try {
            String result = processRequest(input)
                    .collect(Collectors.joining(", ", "[", "]"))
                    .block();
            System.out.println("COMPLETED: " + result);
        } catch (Exception e) {
            System.out.println("ERROR: " + e.getMessage());
        }
    }
}

It prints:

ERROR: Bad request
COMPLETED: [hello, world]
ERROR: No data
Related