Wrapping Exceptions with Context

A good error / exception message should provide enough information to pinpoint a problem. In most modern languages there is a stack trace feature that will show how the program got to the point where it broke.

Unfortunately this information is very static in nature; all the dynamic context is lost. To solve this problem most environments provide exception chaining. Low level exceptions get wrapped into higher level exceptions, that provide context.

Here an example:

/* Posts a comment for a user and returns an id*/
def comment(userId, commmentBody): Id = {
    val user = userReposistory.load(userId)
 
    user.permissions.contains(Permissions.POST_COMMENTS)
    val comment = new Comment(user.id, commentBody, DateTime.now)
    comment.validate()
 
    commentRepository.save(.toJson)
}
 
/* A generic filesystem based json repository */
def save(json: JsonNode): Id = {
    val uuid = generateUuid
    val file = new File(storagePath + "/" + uuid)
    file.write(json.render())
    uuid
}

If this code breaks at the IO level, the information which user tried to post a comment is not no longer in scope. And rightly so, because it’s a separate concern.

The typical way to deal with this is to catch the exception in the comment method and wrap it into a new exception, like so:

def comment(userId, commmentBody): Id = {
    val user = userReposistory.load(userId)
    try {
        user.permissions.contains(Permissions.POST_COMMENTS)
        val comment = new Comment(user.id, commentBody, DateTime.now)
        comment.validate()
 
        commentRepository.save(.toJson)
    } catch {
        case t: Throwable => 
            throw new Exception(s"Exception trying to post comment for user '${user.name}'", t)
    }
}

This will now produce a nice logical stack trace, however it is relatively verbose. Also, the text in the exception, which somehow describes what the code block is doing is on the bottom rather than a proper heading.

I recently debugged a piece of code that I hadn’t touched in two years. The first thing I did was to improve the feedback by wrapping exceptions.

The problem was annoying enough for me to come up with a helper function, aptly called scope, that would let me rewrite the above to:

def comment(userId, commmentBody): Id = {
    val user = userReposistory.load(userId)
    scope(s"post comment for user '${user,name}'") {
        user.permissions.contains(Permissions.POST_COMMENTS)
        val comment = new Comment(user.id, commentBody, DateTime.now)
        comment.validate()
 
        commentRepository.save(.toJson)
    }
}

I find that much more appealing, because the domain logic looks cleaner. This is what the scope function looks like:

def scope[T](scope: String)(expression: => T): T = {
    try {
        expression
    }
    catch {
        case t: Throwable => 
            throw new Exception("Exception trying to " + scope, t)
    }
}

Surely this can be improved. We could have a certain well known exception (super) class that bubbles up the stack without being wrapped. This could be used in cases where the exception is used as a non-local return.

Also, it might be nice to use the same mechanism for logging, so that log messages get context. However in Scala it seems we would have to do this with a side-effect like a thread local, or an extra parameter, as there is no way to put something into the binding of the closure at evaluation time. Tangent: In Kotlin there is a type safe way to do this using its “function type with a receiver”.

In Scala a similar function could be provided for the Try and the Future monads, because they lead to exceptions being passed around without leaving a “stack trace”, so some context would be very helpful indeed.

This entry was posted in Scala, Software Development. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *