An exception thrown in a continuation is implicitly captured by the system and stored in the future. A future that stores such an exception is similar to a ready future in that it can cause its continuation to be launched, but it does not contain a value – only the exception.
Calling .then()
on such a future skips over the
continuation, and transfers the exception for the input future (the
object on which .then()
is called) to the output future
(.then()
’s return value).
This default handling parallels normal exception behavior – if an exception is thrown in straight-line code, all following lines are skipped:
();
line1(); // throws!
line2(); // skipped line3
is similar to
return line1().then([] {
return line2(); // throws!
}).then([] {
return line3(); // skipped
});
Usually, aborting the current chain of operations and returning an exception is what’s needed, but sometimes more fine-grained control is required. There are several primitives for handling exceptions:
.then_wrapped()
: instead of passing the values carried
by the future into the continuation, .then_wrapped()
passes
the input future to the continuation. The future is guaranteed to be in
ready state, so the continuation can examine whether it contains a value
or an exception, and take appropriate action..finally()
: similar to a Java finally block, a
.finally()
continuation is executed whether or not its
input future carries an exception or not. The result of the finally
continuation is its input future, so .finally()
can be used
to insert code in a flow that is executed unconditionally, but otherwise
does not alter the flow.The following example illustrates usage of then_wrapped
and finally
:
#include <seastar/core/future.hh>
#include <iostream>
#include <exception>
::future<> pass() {
seastarstd::cout << "I passed!!!" << std::endl;
return seastar::make_ready_future<>();
}
::future<> fail() {
seastarstd::cout << "I failed." << std::endl;
return seastar::make_exception_future<>(std::exception());
}
::future<> f() {
seastarreturn pass().then([] {
std::cout << "Oh no! I'm gonna fail!" << std::endl;
return fail(); // throws
}).then([] () { // skipped
std::cout << "If I got to this place I will pass!" << std::endl;
return pass();
}).then_wrapped([] (seastar::future<> f) {
if (f.failed()) {
std::cout << "The input future failed!" << std::endl;
return f;
}
std::cout << "If I got to this place I will pass!" << std::endl;
return pass();
}).finally([] () {
std::cout << "This code will run, regardless of any exceptions" << std::endl;
});
}
This time the output will be
I passed!!!
Oh no! I'm gonna fail!
I failed.
The input future failed!
This code will run, regardless of any exceptions
ERROR [...] Exiting on unhandled exception: std::exception (std::exception)
TODO: Also mention handle_exception - although perhaps delay that to a later chapter?
An asynchronous function can fail in one of two ways: It can fail
immediately, by throwing an exception, or it can return a future which
will eventually fail (resolve to an exception). These two modes of
failure appear similar to the uninitiated, but behave differently when
attempting to handle exceptions using finally()
,
handle_exception()
, or then_wrapped()
. For
example, consider the code:
#include <seastar/core/future.hh>
#include <iostream>
#include <exception>
class my_exception : public std::exception {
virtual const char* what() const noexcept override { return "my exception"; }
};
::future<> fail() {
seastarreturn seastar::make_exception_future<>(my_exception());
}
::future<> f() {
seastarreturn fail().finally([] {
std::cout << "cleaning up\n";
});
}
This code will, as expected, print the “cleaning up” message - the
asynchronous function fail()
returns a future which
resolves to a failure, and the finally()
continuation is
run despite this failure, as expected.
Now consider that in the above example we had a different definition
for fail()
:
::future<> fail() {
seastarthrow my_exception();
}
Here, fail()
does not return a failing future. Rather,
it fails to return a future at all! The exception it throws stops the
entire function f()
, and the finally()
continuation does not not get attached to the future (which was never
returned), and will never run. The “cleaning up” message is not printed
now.
We recommend that to reduce the chance for such errors, asynchronous
functions should always return a failed future rather than throw an
actual exception. If the asynchronous function calls another function
before returning a future, and that second function might
throw, it should use try
/catch
to catch the
exception and convert it into a failed future:
void inner() {
throw my_exception();
}
::future<> fail() {
seastartry {
();
inner} catch(...) {
return seastar::make_exception_future(std::current_exception());
}
return seastar::make_ready_future<>();
}
Here, fail()
catches the exception thrown by
inner()
, whatever it might be, and returns a failed future
with that failure. Written this way, the finally()
continuation will be reached, and the “cleaning up” message printed.
Despite this recommendation that asynchronous functions avoid throwing, some asynchronous functions do throw exceptions in addition to returning exceptional futures. A common example are functions which allocate memory and throw
std::bad_alloc
when running out of memory, instead of returning a future. Thefuture<> seastar::semaphore::wait()
method is one such function: It returns a future which may be exceptional if the semaphore wasbroken()
or the wait timed out, but may also throw an exception when failing to allocate memory it needs to hold the list of waiters. Therefore, unless a function — including asynchronous functions — is explicitly tagged “noexcept
”, the application should be prepared to handle exceptions thrown from it. In modern C++, code usually uses RAII to be exception-safe without sprinkling it withtry
/catch
.seastar::defer()
is a RAII-based idiom that ensures that some cleanup code is run even if an exception is thrown.
Seastar has a convenient generic function,
futurize_invoke()
, which can be useful here.
futurize_invoke(func, args...)
runs a function which may
return either a future value or an immediate value, and in both cases
convert the result into a future value. futurize_invoke()
also converts an immediate exception thrown by the function, if any,
into a failed future, just like we did above. So using
futurize_invoke()
we can make the above example work even
if fail()
did throw exceptions:
::future<> fail() {
seastarthrow my_exception();
}
::future<> f() {
seastarreturn seastar::futurize_invoke(fail).finally([] {
std::cout << "cleaning up\n";
});
}
Note that most of this discussion becomes moot if the risk of exception is inside a continuation. Consider the following code:
::future<> f() {
seastarreturn seastar::sleep(1s).then([] {
throw my_exception();
}).finally([] {
std::cout << "cleaning up\n";
});
}
Here, the lambda function of the first continuation does throw an
exception instead of returning a failed future. However, we do
not have the same problem as before, which only happened
because an asynchronous function threw an exception before
returning a valid future. Here, f()
does return a valid
future immediately - the failure will only be known later, after
sleep()
resolves. The message in finally()
will be printed. The methods which attach continuations (such as
then()
and finally()
) run the continuation the
same way, so continuation functions may return immediate values or, in
this case, throw an immediate exception, and still work properly.