I had a similar problem, futures failing silently in cases where the actual result is irrelevant and thus not handled explicitly. From the documentation in ExecutionContext
I initially assumed that the reportFailure
method there was to do reporting for any failure in a Future
. Which is obviously wrong - so this is the approach I came up with to have logged exceptions (even for mapped or otherwise derived) futures:
- a
LoggedFuture
class that delegates to a Future
and onFailure
logs the exception similar to @LimbSoups answer
- for methods like
map
that return a new Future
yield a LoggedFuture
as well
- use a
Promise
as some kind of fail event that is shared between the cascaded LoggedFutures
to log an exception only once even if the onFailure callback is applied multiple times because of the propagation
object LoggedFuture {
def apply[T](future: Future[T])(implicit ec: ExecutionContext): Future[T] = {
if (future.isInstanceOf[LoggedFuture[T]]) {
// don't augment to prevent double logging
future.asInstanceOf[LoggedFuture[T]]
}
else {
val failEvent = promise[Unit]
failEvent.future.onFailure {
// do your actual logging here
case t => t.printStackTrace()
}
new LoggedFuture(future, failEvent, ec)
}
}
}
private class LoggedFuture[T](future: Future[T], failEvent: Promise[Unit], ec: ExecutionContext) extends Future[T] {
// fire "log event" on failure
future.onFailure {
// complete log event promise
// the promise is used to log the error only once, even if the
// future is mapped and thus further callbacks attached
case t => failEvent.tryComplete(Failure(t))
} (ec)
// delegate methods
override def ready(atMost: Duration)(implicit permit: CanAwait): this.type = {
future.ready(atMost)
this
}
override def result(atMost: scala.concurrent.duration.Duration)(implicit permit: CanAwait): T = future.result(atMost)
override def isCompleted: Boolean = future.isCompleted
override def onComplete[U](func: scala.util.Try[T] => U)(implicit executor: ExecutionContext): Unit = future.onComplete(func)
override def value: Option[Try[T]] = future.value
// propagate LoggedFuture (and shared log event) whenever a new future is returned
override def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.map(f), failEvent, executor)
override def transform[S](s: T => S, f: Throwable => Throwable)(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.transform(s, f), failEvent, executor)
override def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.flatMap(f), failEvent, executor)
override def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] =
new LoggedFuture(super.recover(pf), failEvent, executor)
override def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): Future[U] =
new LoggedFuture(super.recoverWith(pf), failEvent, executor)
override def zip[U](that: Future[U]): Future[(T, U)] =
new LoggedFuture(super.zip(that), failEvent, ec)
override def fallbackTo[U >: T](that: Future[U]): Future[U] =
new LoggedFuture(super.fallbackTo(that), failEvent, ec)
override def andThen[U](pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): Future[T] =
new LoggedFuture(super.andThen(pf), failEvent, executor)
}
class RichFuture[T](future: Future[T]) {
def asLogged(implicit ec: ExecutionContext): Future[T] = LoggedFuture(future)
}
Additionally, I have an implicit conversion to RichFuture
(as above) defined so I can easily convert existing futures with calls like future.asLogged
.