I've written some Scala Apis before for things like this and I tend to use the type Either to differentiate between errors and success like this, rather than do the Java-esc thing of throwing exceptions.

case class MyClientError(errorMessage :String)

class MyClient(baseUrl: String) {

def getDataSet(dataSetId: String) : Either[MyClientError,DataSet] = {
  call(dataSetId) match {
     case dataset if result.status == HTTP_OK => Right(dataset)
     case _ if result.status == HTTP_404 => Left(MyClientException("Not found"))
     case result => Left(MyClientException(s"Got non 200 response: ${result.status}")

This way the caller can do this:

getDataSet(54) match {
   case Left(err) => println(s"got an error ${err.errorMessage}")
   case Right(dataset) => println("got a good answer!")

Either tends to compose quite well, can be used in for-yield blocks, is rich in terms of returning a reason for a failure etc. It's easy to turn List(ids) -> Either[List[errors], List[datasets]] etc.


How you handle exceptions on failures is a slightly wider question. Ultimately up to the system designer. You could return the error, put it aside and move on or implement retry logic. Of course, it depends what call() returns when an error occurs. This example demonstrates how you could retry if call() returns a Future that fails with a throwable:

def getDataSet(dataSetId: String, retries :Int = 3) : Future[Either[MyClientError,DataSet]] = {
  call(dataSetId).flatMap {
     case Success(dataset) if result.status == HTTP_OK => Future.successful(Right(dataset))
     case Success(_) if result.status == HTTP_404 => Future.successful(Left(MyClientException("Not found")))
     case Failure(exc) if thr.isTransientError && retries > 0 => getDataSet(dataSetId, retries - 1)
     case Failure(exc) if thr.isTransientError && retries == 0 => Future.successful(Left(MyClientException(s"Tried 3 times but failed: ${exc.getMessage}")))
     case Failure(exc) => Future.successful(Left(MyClientException(s"Unrecoverable: ${exc.getMessage}")))
     case Success(result) => Future.successful(Left(MyClientException(s"Got non 200 response: ${result.status}")))


This is very much a matter of opinion, but I would use exceptions for all error cases. You can define your own HTTP exceptions:

case class MyHttpException(code: StatusCode) extends Throwable

If your client is synchronous then return Try but typically a REST client is asynchronous, in which case you return Future. Both of these object track exceptions and make the Throwable available in the case of error.