The documented behavior is the only thing that matters.
Originally published on May 19, 2016
When we want to write code using a programming language, there is no need to learn the whole specification to understand its works. We read a few trusted resources, try it and see if we can get the desired outcome. If we can't, then we try it again until it works. The specification is used as a reference, not as documentation[1].
As a programmer, you have to understand this.
A programming language is hard to change. It will always thrive on being as backward compatible as possible. Authors of programming languages are usually conservative on what to remove, although there are some cases where updates will break anyway.
That is similar to frameworks. However, unlike languages, a framework can create a "version 2" that rewrites everything. There will be a lot of work, but the responsibility of migrating lies on the consumer side. A framework is pluggable to some extent; a language is not.
As a programmer, you have to understand that too.
However, what happens when you plug a library into your app? A library is supposed to abstract a specific problem; it should be much more open to new changes than a framework. A framework dictates how you code, a library provides particular functionality that should be environment-agnostic. You can replace a library easily, a framework you can't.
In JavaScript, no access modifier can limit a given API's visibility; everything is public by default. It's possible to use closures and other techniques to control access, but that might not be enough to restrict it.
How can we make sure that a library's internal functionality will not be used by the developer that inspects the code using the browser console? How can we make sure the developer knows the contract that the library author provides for consumption without relying on trial and error?
We document it.
Just because it "works", that doesn't mean you're using the library correctly. The documentation, also known as the Public API, serves as a reference to verify if you are using the API correctly. The contract between the author and the consumer is the single source of truth — nothing else.
Everything else is internal, and therefore consumers should assume that it doesn't exist. Even if the author exposes it for convenience, it can be removed anytime without notice, and your app will break.
If the author follows Semver, it is reasonable to remove an exposed (but undocumented) API in a minor or patch version. Semver states that the author should maintain backward compatibility only for the public API:
For this system to work, you first need to declare a public API
— semver.org
There are cases where authors might want to create additional rules and restrictions, depending on the library's purpose.
For all practical purposes, what the author does not explicitly document in the public API does not exist.
Failing to read the docs of a library can cause the system to break because we will rely on how it behaves, not how it's intended to work. It is a recipe for ruining your app any time the patch version is updated instead of breaking only in a major version bump. Even if the system has proper tests, there is no guarantee.
Reading the docs seems so fundamental that one would believe it's common sense. However, I have met many developers that are not aware of the importance of reading the docs, and worse than that, nobody seems to talk about it very often. The lack of this kind of talk forces newcomers to learn the hard way through the experience of creating software that works, and then it doesn't.
How can we make common sense more common?
Here's what happened to me when I started programming many years ago.
jQuery
At the time of writing this, jQuery has the $.trim utility function. It works like this:
[…] removes all newlines, spaces (including non-breaking spaces), and tabs from the beginning and end of the supplied string.
— jQuery documentation
One day I was inspecting the jQuery source code. I noticed it was internally using String.prototype.trim
as the first option if the native trim was available in the browser (probably due to performance reasons). Then it had a fallback for a custom trim that had the restricted documented behavior. String.prototype.trim
does more than just removing spaces and tabs, so I thought it made sense matching the full spec and even suggested it. However, I received feedback that it didn't make any sense because jQuery didn't document the API to replace more than mere spaces and tabs.
The trim() method removes whitespace from both ends of a string. Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.).
— String.prototype.trim on MDN
If these three conditions were true:
- The programmer used
$.trim
as a language, trying to make it work through trial and error. - The browser running the code supported the native trim method.
- The app relied on trimming chars other than spaces or tabs for critical functionality.
Then the application would break once jQuery removed the String.prototype.trim
fallback from the algorithm in a newer version.
Using a library like a programming language will eventually break your system.
In some cases, fixing a bug can break many places because of an implied functionality or philosophy. Take the hash selector breakage, for example, that was a legit bug fix that unfortunately broke an implied contract between a fundamental jQuery philosophy and big consumers of the library (such as WordPress). It's a great example because it showed that the assumption of what is implied between the library author and the consumer is not always clear and can be up to interpretation.
JavaScript Cookie / jQuery Cookie
I also happen to maintain a small library called js-cookie. At the time of this writing, that library had some leftovers from jquery.cookie code. I forked the original code to a new repository, and it carried over an undocumented API: Cookies(String, String)
. However, the only publicly documented APIs are Cookie.get
, Cookies.set
and Cookies.remove
. In this case, because some folks didn't read the documentation, we had reports like this one that showed examples of devs relying on the undocumented API.
The point is: always follow the docs. If there is an internal behavior that you want to depend on, request to update the docs so that the library authors can decide if that should exist as a supported feature or not.
There are cases, though, just like the hash change example from jQuery, where library authors have to fix an undocumented code. That usually happens due to an unexpected violation of the Least Astonishment's Principle coupled with a massive amount of the wrong usage in the wild. Read this thread, and you'll learn a great example, more specifically this comment.
Don't use an internal behavior; start with the docs.
There are some bugs programming language designers can't fix. The wrong usage at scale and the potential of breaking the web make it impossible for them to do so.
Frameworks dictate how you write the code. They should be conservative in what to break, even if it's a functionality that the framework authors haven't documented. However, once in a while, they can change everything so that progress doesn't stop.
JavaScript libraries are pluggable, and their internals is not easy to hide. Therefore they have the opportunity to support only what is publicly documented for the sake of extensibility. But there are exceptional cases in which one should be careful about what to expose.
As a dev, never rely on library internals. In practice, they don't exist.
Edit April 28, 2017:
Among the things that compose your library's public API, method binding might be considered one of them.
Edit March 25, 2021:
It's been four years since I've written this post, and most of it is still relevant today. Even with the widespread usage of TypeScript and the recent plans of introducing Private class fields in the JavaScript ecosystem, the docs remain the accepted canonical source for any library use.
Edit June 03, 2021:
Jake Archibald recently wrote this piece which also brings more clarity to function callback and its public API properties.
1: I’m using the word reference for the cases when you should consult the root specification to learn better about a given behaviour’s specifics. However, in those cases, you may not be required to read the whole specification to know how the system works. You might be using a library that abstracts away the specifics and consults the spec when abstractions leak, such as the case of RFC 6265 vs js-cookie. Also, you might be using a type-safe language where you can learn the basics and then move to trial and error, such as the case of Java spec vs Java lang tutorials.
I’m using the word documentation for how it works, including the basic “Get Started” examples. You have to read everything about the tool you’re using or at least the parts relevant to your case to start using it efficiently. Otherwise, you’ll begin with assumptions that are most likely to be wrong. For example, Luxon, jQuery, and React.
Thanks for reading. If you have some feedback, reach out to me on Twitter, Facebook or Github.
Top comments (0)