What ESLint learned from their 9.0 release
A year after releasing several breaking changes into a single release, ESLint published a retrospective on the process. Their document has good takeaways that are applicable beyond frontend.
In April 2024, ESLint released version 9.0, which contained major breaking changes.
They changed their config format.
They removed support from really old Node.js versions.
They changed defaults in their config file format.
They changed a lot of stuff in their rules engine.
This was a hard upgrade. Since there were so many breaking changes at once, it was difficult to pinpoint a single problem. It’s not hard to find Reddit or Hacker News threads of people complaining about it, but I would just skim Yacine Kharoubi’s blog post about upgrading. The upgrade can be extensive depending on your setup.
Recently, ESLint released a retrospective on the upgrade, with the benefit of 1 year of time between the initial release and now. The retro obviously focuses on the ESLint project, but there are great takeaways even if you’re not a frontend developer.
Users are lazy
Since the upgrade was so extensive, it affected multiple classes of users:
People who produce ESLint rules and plugins.
End users that run ESLint as part of their work, which may also include rules and plugins.
ESLint bet that the plugin and rule developers would be proactive about the change, but expected the end users to lag more. But in reality, nobody was.
There are a trillion concepts that say the same thing. “Inertia.” “Users don’t read.” “Defaults are powerful.” There’s an argument that a proactive plugin developer could get involved early in the development process and provide good advice.
But in reality, who wants to develop against an alpha API? Can you imagine spending a few weeks upgrading your code to account for some hairy change, and then the implementation detail changes and you need to spend that few weeks again? I think the ESLint developers deserve a lot of credit for trying to approach the upgrade thoughtfully, but they would be wise to pay attention to this in future upgrades that affect rules and plugins.
Stay the course
I really appreciated that they stuck to their initial migration timeline.
Some suggested delaying the v9.0.0 release to give the ecosystem more time to catch up. We decided against this for several reasons:
Users weren’t required to upgrade immediately. ESLint v8.x remained fully compatible with the ecosystem and continued receiving bug fixes, so those who didn’t want to upgrade could continue using it.
It was unclear how long such a delay would last. How could we determine when the ecosystem had “caught up”? Should we keep v9.0.0 in limbo while only providing bug fixes for v8.x indefinitely? That didn’t seem like a viable solution.
The
@eslint/eslintrc
package already offered substantial compatibility for eslintrc plugins and shareable configs with v9.0.0, addressing the most common issues we encountered.We had communicated the upcoming changes for 18 months, with increasing reminders as we neared the first alpha release. Although adoption was slower than expected, we saw momentum building and wanted to maintain that pace. Delaying the release could have sent the wrong message and allowed further delays to snowball.
They faced pressure to delay once the community started to popularize the idea that the upgrade was difficult. But without the launch deadline, there wouldn’t be any pressure applied to the lazy developers. Most people would just wait until they were forced to do the upgrade.
Support mitigates pain
A lot has been written about how painful this upgrade is. But first, I want to highlight how thoughtfully the ESLint team approached the upgrade. First, they had a policy of updating the migration guide in the same pull request where changes were introduced.
We also introduced a new process: the v9.0.0 migration guide was updated in the same pull request as each new feature. Previously, we wrote the guide after all features had been merged, increasing the chance of omissions. This new approach helped ensure nothing was missed.
They also had a great support story for their community. They supported version 8 and 9 side-by-side for six months. After that, they had a formalized support policy and an official commercial partner for companies that needed support for version 8 longer than ESLint could provide it. They also improved the migration tools, documentation, and front-line support over time. This allowed them to make migrations easier for people who adopted later.
Six months is not a lot of time in the grand scheme of things. Hell, Python 2 and Python 3 coexisted for a decade. But maintaining out-of-date versions is a major drain on resources, especially for major releases like this. So I commend them for defining an explicit support path.
Avoid bundling lots of breaking changes together
It’s easy to argue in favor of lumping breaking changes together. “You only go through the pain once. We don’t want to have a reputation of always breaking everything on releases. We can always support the two side-by-side for a specific period of time.” The ESLint project even had their own project-specific reasons. “Well, we can’t launch language plugins without all of these features.”
Too many breaking changes
The biggest mistake was bundling too many breaking changes into a single major release. A key example was introducing the new configuration system alongside the rule API changes, both of which were necessary steps to enable language plugins. This often made it difficult to pinpoint the cause of issues with existing plugins. Many assumed the new configuration system was to blame, creating a narrative that it was “broken” or “not ready.” In reality, the rule API changes were just as disruptive.
We were so focused on the configuration system rollout that we underestimated the impact of the rule API changes.
This is a good reminder that the project is about the users. What incentivizes them to upgrade? What do they gain out of it? Is it a slog that they are doing for our benefit? Are the benefits concrete for us and abstract for them?
This isn't a subtweet about the Python 3 rollout, is it? :)