Completable Futures

WARNING

This tutorial assumes that you are familiar with lambda expressions. Take a look at the lambda introduction first, if you are not!

As Javacord is heavily multithreaded, you must understand the concept of Futuresopen in new window in general, as well as their most common implementation, the CompletableFutureopen in new window. This little introduction gives you a quick overview of the basics you need to know in order to work with Futures.

🤔 What the heck is a future?

A future is basically a wrapper, that will contain a value in the future but might not contain it right now. This is useful, if a method call requires some time and should not block the execution of your current code. You can easily see the difference with a primitive speed comparison:

long currentTime = System.currentTimeMillis();
channel.sendMessage("Test 1");
channel.sendMessage("Test 2");
channel.sendMessage("Test 3");
channel.sendMessage("Test 4");
channel.sendMessage("Test 5");
// Prints "4 ms"
System.out.println((System.currentTimeMillis() - currentTime) + " ms");
long currentTime = System.currentTimeMillis();
channel.sendMessage("Test 1").join();
channel.sendMessage("Test 2").join();
channel.sendMessage("Test 3").join();
channel.sendMessage("Test 4").join();
channel.sendMessage("Test 5").join();
// Prints "894 ms"
System.out.println((System.currentTimeMillis() - currentTime) + " ms");

TIP

join() blocks the current thread until the method finished. This will be explained later.

📖 Methods

join()

The join method blocks the current thread until the method finished. It returns the method's result or throws a CompletionException if anything failed.

The following example would create a new text channel in a given server and sends a message directly afterwards.

// Create the channel
ServerTextChannel channel = new ServerTextChannelBuilder(server)
    .setName("new-channel")
    .create()
    .join();
// Send a message in the new channel
Message message = channel.sendMessage("First!").join();
// Adds an reaction to the message. Even though this method doesn't return anything,
// join() ensures, that an exception is thrown in case something went wrong
message.addReaction("👍").join();

DANGER

You should avoid join() for methods which will be called frequently.

TIP

While join() can become a performance issue when you call it very frequently, it is very convenient to use and easy to understand. If you are new to programming and just want to get your first bot working, this is a good method to start with.

Once you gathered more experience, we highly advise against using join as it negatively impacts your bot's performance!

thenAccept(...)

The thenAccept method accepts a Consumer, that consumes the result of the method and is executed asynchronously. It is the method you usually want to use most of the time.

The following example would create a new text channel in a given server and send a message directly afterwards.

new ServerTextChannelBuilder(server)
    .setName("new-channel")
    .create()
    .thenAccept(channel -> {
        channel.sendMessage("First!").thenAccept(message -> {
            message.addReaction("👍");
        });
    });

DANGER

The example code above has a major problem: Any exception that might occur will be completely ignored. This makes it very hard to find bugs.

For example, if your bot doesn't have the permissions to create a new channel, it will just fail silently.

exceptionally(...)

The exceptionally method accepts a Function as parameter, which consumes possible exceptions and returns a fallback value.

The following example would create a new text channel in a given server and send a message directly afterwards. If something fails (e.g., if the bot isn't allowed to create a text channel in the server), it will log an exception.

new ServerTextChannelBuilder(server)
    .setName("new-channel")
    .create()
    .thenAccept(channel -> {
        channel.sendMessage("First!").thenAccept(message -> {
            message.addReaction("👍").exceptionally(e -> {
                e.printStackTrace(); // Adding the reaction failed
                return null;
            });
        }).exceptionally(e -> {
            e.printStackTrace(); // Message sending failed
            return null;
        });
    }).exceptionally(e -> {
        e.printStackTrace(); // Channel creation failed    
        return null;
    });

Wow! This looks ugly 🤮. But worry not! There are many options to improve this code!

To make things simpler for you, Javacord has the ExceptionLogger class, which can be used here. It logs every exception you didn't catch manually.

new ServerTextChannelBuilder(server)
    .setName("new-channel")
    .create()
    .thenAccept(channel -> {
        channel.sendMessage("First!").thenAccept(message -> {
            message.addReaction("👍").exceptionally(ExceptionLogger.get());
        }).exceptionally(ExceptionLogger.get());
    }).exceptionally(ExceptionLogger.get());

Okay! This is at least a little better, but still not really perfect 🤔.

thenCompose()

The thenCompose methods allows you to chain futures. It takes a Functionopen in new window as parameter, that consumes the future's value and expects a new future to be returned.

The example to create a text channel can now be written like this:

new ServerTextChannelBuilder(server)
        .setName("new-channel")
        .create() 
        .thenCompose(channel -> channel.sendMessage("First!"))
        .thenCompose(message -> message.addReaction("👍"))
        .exceptionally(ExceptionLogger.get());

Finally 🎉! Now we only need a single exceptionally(...) call at the end. We also got rid of the nested callbacks (usually referred to as "callback hell").

For better understanding, here's the example with comments that tell you the type at each line:

new ServerTextChannelBuilder(server) // ServerTextChannelBuilder
        .setName("new-channel") // ServerTextChannelBuilder
        .create() // CompletableFuture<ServerTextChannel>
        .thenCompose(channel -> channel.sendMessage("First!")) // CompletableFuture<Message>
        .thenCompose(message -> message.addReaction("👍")) // CompletableFuture<Void>
        .exceptionally(ExceptionLogger.get()); // CompletableFuture<Void>

📚 Further Read

This tutorial only focuses on the absolute basics. For a more detailed introduction to CompletableFutures, you can take a look at this tutorialopen in new window.

You should also take a look at the JavaDoc for a complete list of methods: CompletableFuture JavaDocopen in new window.