Skip to main content

Error Handling

note

For a more detailed source of information, please refer to this section of the Reactor reference guide.

According to reactive streams specification, errors are terminal signals. This typically means any running sequence will be terminated and the error propagated to all operators down the chain.

Handling Errors#

The following are some valid strategies for dealing with errors:

Accept the error#

Error will propagate downstream until the end of the chain and then run the onError callback in your subscriber.

You can optionally log and react on the side using doOnError operator.

As good practice, we encourage you to implement the onError callback when subscribing to be properly notified. If you don't override the onError callback, you will receive a Reactor ErrorCallbackNotImplemented exception wrapping your original exception.

client.getEventDispatcher().on(MessageCreateEvent.class)    .map(MessageCreateEvent::getMessage)    .filter(message -> message.getAuthor().map(user -> !user.isBot()).orElse(false))    .filter(message -> message.getContent().equalsIgnoreCase("!ping"))    .flatMap(Message::getChannel)    .flatMap(channel -> channel.createMessage("Pong!"))    .doOnError(error -> { /* You can be notified here as well! */ })    .subscribe(null, error -> {        // the error signal will stop here and terminate the sequence        System.out.println(e);    });

โš ๏ธ Taking this approach under EventDispatcher sequences will terminate your subscription, missing all further events for that subscriber. In general, this is a poor solution if you wish to perform extra behavior like starting another chain.

Catch and return a static default value: onErrorReturn#

โœ”๏ธ This approach is good for individual API requests when you want to return a value in case of an error.

โš ๏ธ Taking this approach with EventDispatcher will terminate the sequence, and no further events will be received by that subscriber, unless you apply this operator to an inner sequence, without affecting the outer one containing all events, like this example:

client.getEventDispatcher().on(MessageCreateEvent.class)    .map(MessageCreateEvent::getMessage)    .filter(message -> message.getAuthor().map(user -> !user.isBot()).orElse(false))    .filter(message -> message.getContent().startsWith("!user "))    .flatMap(message -> Mono.just(message.getContent())        .map(content -> content.split(" ", 2))        .flatMap(tokens -> message.getClient().getUserById(Snowflake.of(tokens[1])))        .map(user -> user.getUsername() + "#" + user.getDiscriminator())            .onErrorReturn("Could not find that user") // Replaces errors with this msg        .flatMap(name -> message.getChannel()            .flatMap(channel -> channel.createMessage(name))))    .subscribe(null, System.out::println);

onErrorReturn has overloads to include a condition so you could for example use the following to only recover from 404 status errors (Not found):

.onErrorReturn(ClientException.isStatusCode(404), "Could not find that user")

Catch and execute an alternative path with a fallback method: onErrorResume#

โœ”๏ธ This approach is good for individual API requests when you want to provide alternative behavior.

โœ”๏ธ This approach is great when working inside a flatMap with EventDispatcher, as you will replace the sequence with, for example, Mono.empty() effectively suppresing the error while maintaining the original sequence.

Flux.just("๐Ÿ˜€", "๐Ÿ˜ฌ", "๐Ÿ˜‚", "๐Ÿ˜„")    .flatMap(emoji -> message.addReaction(ReactionEmoji.unicode(emoji))        .onErrorResume(e -> Mono.empty()) // error is discarded    )    .subscribe(); // so it won't get here

If you were to place onErrorResume outside a flatMap, you'll replace the sequence, potentially missing some elements being processed:

Flux.just("๐Ÿ˜€", "๐Ÿ˜ฌ", "๐Ÿ˜‚", "๐Ÿ˜„")
    // if this fails on the 3rd emoji    .flatMap(emoji -> message.addReaction(ReactionEmoji.unicode(emoji)))
    // you'll replace the sequence with an empty one, and miss the last one    .onErrorResume(e -> Mono.empty())
    // but the error still won't reach here!    .subscribe();

Catch and Rethrow: onErrorMap#

โœ”๏ธ This approach is good for individual API requests when you want to translate the error, typically ClientException, to a type you control for additional behavior downstream.

โœ”๏ธ This approach is good when working with EventDispatcher for the same reason as above. Be aware that the sequence is still on error and can be handled by a different strategy on following operators.

Retrying: retry, retryWhen#

Error will terminate the original sequence, but retry() (and variants) will re-subscribe to the upstream Flux. Be aware that this ultimately means that a new sequence is created.

client.getEventDispatcher().on(MessageCreateEvent.class)    .map(MessageCreateEvent::getMessage)    .filter(message -> message.getAuthor().map(user -> !user.isBot()).orElse(false))    .filter(message -> message.getContent().equalsIgnoreCase("!ping"))    .flatMap(Message::getChannel)    .flatMap(channel -> channel.createMessage("Pong!"))    .retry()    .subscribe();

โš ๏ธ This approach is generally appropriate for API requests, but there are certain errors you should not retry. By default, Discord4J retries some errors for you, using an exponential backoff with jitter strategy.

โœ”๏ธ This approach is compatible with EventDispatcher sources when using the default event processor, due to it only relaying events since the time of subscription. As retrying creates a new subscription, the erroring event will be discarded and the sequence will continue from the next event.

Catch and continue mode#

onErrorContinue#

โš ๏ธ This only works on supporting operators: flatMap, map and filter, among others according to their javadocs. This operator goes beyond the Reactive Streams spec and uses the Reactor Context to work, therefore it is prone to issues when combining it with unsupported operators. Only use this operator if you understand the consequences, or you're familiar with how Reactor Context works.

Applying onErrorContinue on a Flux will change the default behavior of treating errors as terminal events to discarding erroneous elements and keeping the same sequence active.

We do not recommend the usage of this operator for error handling purposes. Instead, try this "resuming to empty" pattern, preventing the error from continuing downstream:

...flatMap(source -> reactiveOperationThatMightError()        .doOnError(error -> log.info("Error encountered while processing {}", source, error))        .onErrorResume(error -> Mono.empty()))

onErrorStop#

Using onErrorStop will revert the behavior to treating errors as terminal events. This can be used to accurately scope continue strategy and avoid surprises, specially when combining it with onErrorResume.

Error Sources#

Typical Reactor operators will throw errors if you:

  • Throw any RuntimeException inside a lambda within an operator (see 4.6.2 for an in-depth explanation)
Flux.just(1, 2, 0)    .map(i -> "100 / " + i + " = " + (100 / i)) // this triggers an error with 0    .subscribe();
  • Transform a signal into an error one
Flux.just("Mega", "Micro", "Nano")    .flatMap(s -> {        if (s.startsWith("M")) {            return Mono.just(s);        } else {            return Mono.error(new RuntimeException());        }    })    .subscribe();
  • Receive an HTTP error code (400s or 500s)
client.getEventDispatcher().on(MessageCreateEvent.class)    .map(MessageCreateEvent::getMessage)    .filter(message -> message.getAuthor().map(user -> !user.isBot()).orElse(false))    .filter(message -> message.getContent().equalsIgnoreCase("!ping"))    .flatMap(Message::getChannel)    .flatMap(channel -> channel.createMessage("Pong!")) // can fail with 403, 500, ...    .subscribe();
  • Return null (except some documented cases)
Flux.just(1, 2, 3)    .map(n -> null) // illegal operation    .subscribe();
  • Overflow due to not generating enough demand
// Generate a tick every 10 msFlux.interval(Duration.ofMillis(10))    //.onBackpressureDrop() // uncommenting this avoids the error: drop tick if consumer is choked    .flatMap(tick -> Mono.never()) // would "never" consume the upstream ticks, overflows    .subscribe();

Handling errors across multiple requests using Discord4J#

Until now, we have seen examples that deal with error handling on particular sequences, and while you should continue to use these patterns for most use cases, you might find yourself applying the same operator to a lot of requests. For those cases, Discord4J provides a way to install an error handler across many or all requests made by a DiscordClient.

When you build a Discord4J client through DiscordClientBuilder you'll notice that there are many setters for a variety of customization. You can handle errors in multiple requests by providing a custom ResponseFunction through onClientResponse method.

You could, for example, build your clients this way:

import discord4j.rest.http.client.ClientException;import discord4j.rest.request.RouteMatcher;import discord4j.rest.response.ResponseFunction;import discord4j.rest.route.Routes;import reactor.retry.Retry;
import java.time.Duration;
public class ExampleClientResponse {
    public static void main(String[] args) {        DiscordClient client = DiscordClientBuilder.create(token)            // globally suppress any not found (404) error            .onClientResponse(ResponseFunction.emptyIfNotFound())            // bad requests (400) while adding reactions will be suppressed            .onClientResponse(ResponseFunction.emptyOnErrorStatus(                    RouteMatcher.route(Routes.REACTION_CREATE), 400)            )            // server error (500) while creating a message will be retried with backoff            // until it succeeds            .onClientResponse(                ResponseFunction.retryWhen(                        RouteMatcher.route(Routes.MESSAGE_CREATE),                        Retry.onlyIf(ClientException.isRetryContextStatusCode(500))                            .exponentialBackoffWithJitter(                                Duration.ofSeconds(2), Duration.ofSeconds(10)                            )                )            )            // wait 1 second and retry any server error (500)            .onClientResponse(ResponseFunction.retryOnceOnErrorStatus(500))            .build();    }}

Each time onClientResponse is called, you're adding a strategy to transform each response made by the DiscordClient. If an error occurs, Discord4J processes the error through the following handlers:

  1. Handle rate limiting errors (429), these cannot be modified.
  2. Handle the errors using the ones installed by onClientResponse.
  3. Handle server errors (500s) and retry them using exponential backoff.

The first handler that matches will consume the error and apply its strategy, meaning that the order of declaration is important.

You can look at the ResponseFunction class for commonly used error handlers. A version covering all requests is available, but also a version allowing you to apply the handler to only some API Routes, with the support of RouteMatcher. Explore the Javadocs for the rest module to understand more.