API methods should not lie

July 1, 2015

It took me a while to figure out exactly why I was so frustrated with some of our code. I originally thought that it might be the sheer amount of boilerplate code or the fact that I had to recompile whenever I made a change that frustrated me. But it came down to one simple issue: the API I was using was lying to me. Yes, you read that right. The API provided to me was behaving in a way that didn't match my expectations. Let me explain what happened: This story starts with me working on some bug-tickets around widget-types. Here's the API I had been given:

package modules.liars  
{  
    public class IWidgetTypeInfoProxy  
    {  
        public function getWidgetTypeInfo(widgetType):IWidgetTypeInfo;  
        public function getAllWidgetTypes():Array;  
    }  
}

Here's how we had been using this API:

var wti:widgetTypeInfo = widgetTypeInfoProxy.getWidgetTypeInfo(widgetType);

Do you see that little unassuming function called “getWidgetTypeInfo”?

That little function was at the root of many headaches because sometimes it would return a widget-type, and sometimes it would return null. It was all kind of random, or so I thought. Issues ranged from:

  1. Users not being able to navigate the application
  2. The user interface remained semi-loaded
  3. Functionality that was permitted like importing would run time exception

It turns out that when this code was initializing, it was making an RPC to load the widget-types, and sometimes when we went to return one of the widget-types, it wasn't finished loading, so it returned a null.

This was the first time I caught an API lying.

See that function definition? Yeah, the one that looks like it immediately returns a widget-type?

public function getWidgetTypeInfo(widgetType):IWidgetTypeInfo;

To me, it implies that I can make that call, and since it's a synchronous function, it will block execution until it returns the widget-type...and most of the time, that's what it would do. But sometimes, it would return an unexpected null and cause havoc with the rest of the code.

Here's why that function was lying.

This function was hiding a dirty little secret. Sometimes, the data that it relied on wasn't yet loaded. Did you notice how the API gives no indication of this? This is a problem between what the public API methods declared versus how the method was actually implemented. The public interface lies about how it is implemented. The public method says: "I am synchronous," but the actual implementation is anything but synchronous. So what now? How are developers supposed to reason about an API when they can't trust what it is telling them? Are they expected to peek into the implementation and modify their expectations? Of course not, that's ridiculous.

A truthful API

How can we rewrite this API to be more truthful about the work that is done and how that work is executed? Here's one way that we've found we can communicate how the methods are implemented in the method signatures themselves:

package modules.truth
{
    public class IWidgetTypeInfoProxy
    {
        public function getWidgetTypeInfo(widgetType):Future.;
        public function getAllWidgetTypes():Future.;
    }
}

What is a “future”?

Futures provide a nice way to reason about performing many operations in parallel—in an efficient and non-blocking way. The idea is simple, a future is a sort of a placeholder object that you can create for a result that does not yet exist. Generally, the result of the future is computed concurrently and can be later collected. Composing concurrent tasks in this way tends to result in faster, asynchronous, non-blocking parallel code.

—Futures and Promises, Scala Documentation

It indicates that callers of this function must treat this as a value that may not be immediately available. Let's look at how the calling code could be rewritten to make use of this truthful API:

widgetTypeInfoProxy.getWidgetTypeInfo(widgetType)
     .then(handleWidgetTypeLoaded)
     .catchError(handleError)

Now, callers will immediately get a future back. When we've completed loading the widgets, we can just complete the future at that time. See what we did there?

  • The method signature communicates the async nature of this method
  • Callers of this API don't need to understand the implementation details as the method communicates the expected behavior (sync vs. async)
  • The user isn't even given the option of calling the method in a synchronous manner—which is a really good thing. We're now discouraging callers of this API from hanging themselves with race conditions
  • Callers of this API are encouraged to compose the error-handling alongside the success-handling resulting in cleaner code

The truth here

If the implementation of an API method is inherently asynchronous, then the method signature must explicitly indicate that it is asynchronous.