Strict vs. Forgiving APIs

Suppose it's the early 1990's and you're James Gosling implementing String.substring(int, int) for the first time. What should happen when the index arguments are out-of-range? Should these tests pass? Or throw?
  public void testSubstring() {
assertEquals("class", "superclass".substring(5, 32));
assertEquals("super", "superclass".substring(-2, 5));
assertEquals("", "superclass".substring(20, 24));
assertEquals("superclass", "superclass".substring(10, 0));

Forgiving APIs

In a forgiving API, these tests pass. The implementation would recognize the out-of-range indices and correct for them. Benefits of forgiving APIs:
  • Fault-tolerant. An off-by-one mistake won't bring a production system to its knees.
  • Easier to code against. If you don't know what to use for a given argument, just pass null and the implementation will do something reasonable.

Strict APIs

In a strict APIs, the out-of-range arguments to substring are forbidden and the method throws an IllegalArgumentException. Benefits of strict APIs:
  • Fail-fast. An off-by-one mistake will be caught in unit tests, if they exist.
  • Easier to maintain. By limiting the number of valid inputs, there's less behaviour to maintain and test.
  • More Predictable. Mapping invalid inputs to behaviour is an artform. In the example, should substring(10, 0) return the empty string? Or "superclass"? What would the caller expect?

For maintainability, I almost always prefer strict APIs. I like to think of the classes in my code as the gears in a fine Swiss watch. Everything fits together tightly, with firm constraints on both the inputs and the outputs. I can refactor with confidence because the system simply won't work if I've introduced problems into it. With a forgiving API, I could introduce bugs and not find out about them until much later.
Good post.

But, pathetically bad skin for your blog. Can't see what is in bold and what is not.
Many of the frameworks I wrote many years ago (and we are still stuck with them) use "forgiving" APIs and do nasty things like convert nulls to empty strings or zeros, in a naive effort to avoid NullPointerExceptions. This is all deeply entrenched legacy code now (more than 5 years old) and we have no reasonable way to move away from it. In many cases, you won't see the true cost of forgiving APIs that let bad inputs seep through until many years later in a project. But now it is too late, so we must continually add layers of additional validation, error checking, and even weird flags to represent "null" rather than "0". My philosophy has definitely moved towards strict APIs and fail-fast, perhaps because I've had the unique opportunity to both develop major frameworks as well as live with the consequences of my frameworks for several years now.
I'm with you - strict APIs are the way to go, as they are more transparent. It's not always clear what a forgiving API is doing to "help" you.
I definitely agree that strict APIs are the way to go. And your post nicely frames the reality of a time when forgiving APIs were fashionable in that unit tests were not the norm.

Unit tests really are game changing.