1. Introduction

In Scala, Futures are a powerful way of performing asynchronous computation. However, they are somehow low-level and often difficult to compose and work with. scala-async tries to solve this issue by letting programmers compose them in a more readable way.

scala-async is a library aimed at defining a Domain-Specific Language (DSL) to compose Futures more easily. It’s used by a number of open-source projects including Coursier.

In this tutorial, we’ll look at scala-async and see how to use its async() and await() constructs. We’ll also go through their limitations and compare them with the native Future API.

2. scala-async

In this section, we’ll see how we can use scala-async to enable an easier composition of Scala Futures.

2.1. Required Dependencies

scala-async 1.0.0 onwards requires at least Scala 2.12.12 or 2.13.3. At the time of writing, it’s not available for Scala 3.

To use scala-async in our code, we’ll have to add the following dependencies:

libraryDependencies += "org.scala-lang.modules" %% "scala-async" % "1.0.1"
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided

Note the Provided dependency on scala-reflect.

Lastly, scala-async requires the -Xasync compiler option:

scalacOptions += "-Xasync"

2.2. async() and await()

scala-async makes use of two fundamental constructs – async() and await().

The former marks a block of code as asynchronous. Usually, such a block contains one or more calls to await, marking points where the computation will be suspended until the corresponding Future is complete.

Let’s see how scala-async defines these two methods:

def async[T](body: => T)(implicit execContext: ExecutionContext): Future[T]

def await[T](awaitable: Future[T]): T

The actual implementations of async() and await() make use of Scala macros and are therefore out of the scope of this article.

async() is just a function that takes a by-name parameter representing a generic body of the asynchronous block, and runs it. Since it starts an asynchronous computation, we also have to pass an (implicit) ExecutionContext. The body parameter in async() may contain one or more calls to await().

await() takes a Future[T] and returns its value. Internally, it registers the remainder of the async() code in the onComplete() method of the awaitable Future, to avoid blocking a thread. By default, scala-async makes use of Promises to run async() blocks.

Let’s see an example. Let’s start by defining a couple of Futures. slowComputation() simply blocks for one second before returning the Int 10:

def slowComputation = Future {
  Thread.sleep(1000)
  10
}

anotherSlowComputation() blocks for one and a half seconds before returning the length of a String passed in as a parameter:

def anotherSlowComputation(s: String) = Future {
  Thread.sleep(1500)
  s.length
}

Then, let’s combine them with scala-async, returning the sum of their values:

def sequentialCombination = async {
  await(slowComputation) + await(anotherSlowComputation("Baeldung"))
}

The Scala compiler analyzes the content of the async() block to identify the await() calls in it, transforming it into non-blocking code:

whenReady(sequentialCombination) { r =>
  assert(r == 18)
}

Finally, we leveraged ScalaFutures::whenReady() to have our ScalaTest test case wait until the composite Future is ready.

2.3. Parallel Composition

In the example above, the Futures will be composed sequentially. In particular, we first asynchronously wait for slowComputation() to finish. When that happens, we wait for anotherSlowComputation(“Baeldung”) to finish. Lastly, we calculate the final result. Basically, that way of composing Futures means awaiting them one by one.

Let’s see how we can work around this:

def parallelCombination: Future[Int] = async {
  val r1 = slowComputation
  val r2 = anotherSlowComputation("Baeldung")
  await(r1) + await(r2)
}

whenReady(parallelCombination) { r =>
  assert(r == 18)
}

Now, when we write val r1 = slowComputation, we trigger the computation of the first Future, without waiting for it to complete. Similarly for r2. Then, when we get to the first await, we’re still asynchronously waiting for slowComputation() to complete, but both Futures are running concurrently.

await(slowComputation) essentially wraps the rest of the async() block in the onComplete() callback of slowComputation(). Hence, if we trigger the second Future after the call to await, the second computation will be started after slowComputation completes. Instead, if we trigger it before, the second computation will be running concurrently with the first. When the first computation completes, the second might as well have already finished.

3. Limitations

There are a couple of limitations to the use of await. First, we must use it at the top level of the corresponding async() call. In other words, we can’t nest await() in a local function, object, or class.

Let’s see an example:

def invalid = async {
  def localFunction = {
    await(slowComputation)
  }

  localFunction
}

In the example above, we used await() within a nested function, localFunction(). Here’s the error that we get if we run it:

await must not be used under a nested method.
      await(slowComputation)

Furthermore, we can’t use await() within a try/catch/finally block. However, according to the GitHub page of scala-async, such a limitation may be removed in the future.

Let’s see an example:

def tryCatch = async {
  try {
    await(slowComputation)
  } catch {
    case e: Throwable => println(e.getMessage)
  }
}

Again, we’re nesting await() within a try. Here’s the error that we get if we run it:

await must not be used under a try/catch.
      await(slowComputation)

Therefore, if we were to need Future composition in a nested local function, object, class, or in a try/catch/finally block, we’d have to fall back on for comprehension, which we’ll tackle next.

4. Comparison With Future API

What scala-async does can be achieved also using Scala Futures together with for comprehension (or Future::map and Future::flatMap):

def withFor = for {
  r1 <- slowComputation
  r2 <- anotherSlowComputation("Baeldung")
} yield r1 + r2

Here, slowComputation() and anotherSlowComputation(“Baeldung”) will be evaluated sequentially, as in serialCombination() earlier.

scala-async has two main advantages over such a way of composing Futures:

  • the version of the code using async() and await() is more readable, as programmers can write it as if it’s blocking, when, in fact, it’s non-blocking. Furthermore, in sequentialCombination(), we didn’t have to name the intermediate results.
  • async() blocks are compiled into a single anonymous class. On the other hand, Scala compiles the generators of the for comprehension into a separate anonymous class for each closure. Hence, scala-async reduces the size of the generated code and can avoid the boxing of intermediate results.

5. Conclusion

In this article, we looked at scala-async, a DSL to help us compose Scala Futures more easily. We saw some examples of the main two constructs, async() and await(), as well as their limitations. Lastly, we compared scala-async with the direct use of the native Future API.

As usual, the source code for the examples is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.