Previous Entry Share Next Entry
Enabling strongly-typed Exceptions with Autowire
querki
jducoeur wrote in querki_project
As mentioned in the most recent release notes, today's project was all about figuring out how to propagate strongly-typed Exceptions through Autowire. I figure that, having done that work, I should write it up so others can adopt the model to their code as desired.

I should say upfront that functional purists may want to give this article a pass. If you're of the school that says that all information returned from a function belongs in its signature, then this technique is a non-sequiteur: you should be returning something Try-like from your APIs. But there are a lot of us soft-functional programmers who still use Exceptions routinely; this is for that crowd.

In the description below, I'm going to include a bunch of example code, all of which is untested. It is basically all massive simplifications of my real code, which I will link to in the discussion. (Warning: lots and lots of GitHub links ahead.) The examples shown should give you the idea, but go to the links for the gory details.


Motivating Example: being essentially a database system, one of the more important aspects of Querki is data entry; you can put an edit field for a property on your page simply by saying [[My Property._edit]]. But not all input data is legal. Querki's type system, buried way down deep inside the server, automatically does validation of all submitted input, and kicks out exceptions when that doesn't pass. The UI to date has translated and re-translated those exceptions, genericizing them and making them hard to use, with the result that they simply showed up as error messages at the top of the page. The immediate motivation for today's project was to pass strongly-typed ValidationExceptions from the point where the error happened up to the call in the Client, and have the Client render that error message in a precise and appropriate way.

Suffice it to say, that's now working. And while it took about six hours to get working, the *vast* majority of that time was setting up the plumbing. In good Autowire style, now that that's done, adding and using new Exceptions is nigh trivial.


Let's walk from the point of the Exception, up to the place where it's being used.

First, we have to define our actual Exceptions:


sealed trait ApiException extends Exception
case class ValidationException(msg:String) extends ApiException



Note that our Exceptions all inherit from a single sealed trait. That's not quite ideal from a coupling POV, but bearable for API purposes, and makes it much easier to just serialize them using uPickle.

Next, there is the validation function itself, in this case the Integer validator. This kicks out the error, in the form of a ValidationException:


if (v.length == 0)
    throw new ValidationException("Numbers may not be empty")



Your Autowire router needs to think about its Futures more completely now, using onComplete instead of just foreach, and kicking a signal back to the HTTP level saying whether the result is good or not, pickling the exception it there is one:


route[EditFunctions](new EditFunctionsImpl)(req).onComplete { 
    case Success(result) => senderSaved ! ClientResponse(result)
    case Failure(ex @ ApiException) => senderSaved ! ClientError(write(ex))
}



Up at the front of the web service, you take that signal and translate it into an HTTP error -- in this case, a 300:


askUserSpaceSession(rc, ClientRequest(request, rc)) {
    case ClientResponse(pickled) => Ok(pickled)
    case ClientError(msg) => BadRequest(msg)
}





So at this point, we now have our web server spitting out either a normal 200 (Ok) response if the Autowire function worked properly, or a 300 (Bad Request) if something went wrong. Now, the client-side AJAX code needs to take note of that, and begin to turn things back into exceptions:


  val deferred = call.ajax(settings).asInstanceOf[JQueryDeferred]
  deferred.done { (data:String, textStatus:String, jqXHR:JQueryXHR) => 
      promise.success(data)
  }
  deferred.fail { (jqXHR:JQueryXHR, textStatus:String, errorThrown:String) => 
      promise.failure(PlayAjaxException(jqXHR, textStatus, errorThrown))
  }



That PlayAjaxException propagates back up to our Client code, specifically in our doCall() implementation for Autowire. This intercepts the PlayAjaxException and tries to unpickle it as an ApiException instead:


controllers.ClientController.apiRequest(
    DataAccess.userName, 
    DataAccess.spaceId.underlying).callAjax.transform(
      { result => result },
      { ex =>
        ex match {
          case ex @ PlayAjaxException(jqXHR, textStatus, errorThrown) => {
            try {
              val aex = read[querki.api.ApiException](jqXHR.responseText)
              throw aex
            } catch {
              case aex:querki.api.ApiException => throw aex
              // The server sent a non-ApiException, which is unfortunate. Just display it:
              case _:Throwable => ... // do something about unknown exceptions
            }
      })



So you've now pulled the ApiException out of the HTTP response, deserialized it and thrown it into the Future that you returned from doCall. The last step is simply to handle that at the point of call, in normal Scala fashion:


Client[EditFunctions].alterProperty(thingId, msg).call().onComplete {
  case Success(response) => ... // tell the user that we've saved successfully
  case Failure(ex) => {
    ex match {
      case querki.api.ValidationException(msg) => {
        showValidationError(msg)
      }
    }
  }
}



That's pretty much it. Summarizing, the steps are:

  • Define the API exceptions in shared code

  • Throw the API exception in the server

  • Catch that in your Autowire router, pickle it and forward it

  • At the HTTP level, return Exceptions differently than you do success values

  • In the Client, handle those error codes and turn them into wrapper exceptions

  • In your doCall() implementation, handle the wrapper Exceptions and unpickle the API Exception

  • Handle the API Exception from your application code as normal



Most importantly, note that you only have to do all those steps *once*. In good Autowire fashion, once the plumbing is done, adding more Exceptions is near-trivial: just define the Exception, throw it on the Server and catch it on the Client. No boilerplate required!

Honestly, this pushes Autowire into nigh-unbelievably cool territory. The end result here is that you can do client/server programming, strongly typed end-to-end, *including* full Exception propagation, with pretty much minimal effort. I know of no other system that even comes close to making it this easy -- and we're still in early days yet. It is really starting to demonstrate why Scala.js and its tools are game-changers for application programming...
Tags:

?

Log in

No account? Create an account