On being an idiomatic JavaScript programmer

When I first started out using C++ in college around 2004, I constantly felt like I was in over my head. In my first semester of C++, I can still remember how hard writing a simple algorithm like "Finding the largest integer in a list" was. After a number of years of programming, those impossible tasks became easier and my focus shifted from solving to solving elegantly. I've found this "elegance" to change from language to language. It's often referred to as being idiomatic.

Years ago, I started learning Ruby and one of the things I loved about it is how opinionated it is and how Rubyists have a "feel" for what is idiomatic and what is not. I think The Ruby Style Guide and Rubocop are amazing projects for people who need training wheels for writing idiomatic Ruby, but a static code analyzer can only go so far. I think it takes time before you can begin to "think" and reason in a language in a way that feels natural to others.

This reminds me of how I tried to translate from English to Japanese when I was first learning. I would say "kikunai" which is the verb "to hear" negated, but that is not what natives say. Natives say, "did not enter the ear" (mimi ni hairanai) — very literal. I believe the same holds true of programming languages and you have to learn the nuances and patterns that develop around a community.

Linters and style guides can help guide some, but I've found general, high-level guidelines to be the quickest way to understand the ethos a community has agreed upon within a language. The best example would be Python's The Zen of Python. Dave Cheney followed suit and wrote the Zen of Go, which I think is great, but as a budding Gopher — it still leaves me with questions such as:

  • When do I abstract to packages?
  • How do I think about package naming to avoid stutter?
  • I see a lot ofsingle letter variable names in Go code. What makes that idiomatic?

I've thought about what it means to be idiomatic for years with the languages I've spent the most time in, JavaScript and Typescript. In 2020, I still don't have an answer. Sure, you can install eslint and format your code with prettier, but that doesn't dictate how things are written. Unlike Ruby and Go where codebases typically share similar structure and style, I've found JavaScript / Typescript to be a free-for-all.

I don't think this is really anyone's fault. The amount of change that the landscape has undergone over the past five years and continues to go through is enormous, which I think leads to a lot of variance in how things are written. The progression in JavaScript and Typescript comes at a tremendous expense: mastery. You can't master something that is ever-changing.

Progress in JavaScript

Did you know that JavaScript (ECMAScript) has been on a yearly release schedule since June of 2015? This is why ES6 is now ES2015.

On June 17, 2015, ECMA International published the sixth major version of ECMAScript, which is officially called ECMAScript 2015, and was initially referred to as ECMAScript 6 or ES6. Since then, ECMAScript standards are on yearly release cycles.

So, in ES2020 JavaScript will now support these features (via v8.dev):

Nullish coalescing
Optional chaining
globalThis
Promise combinators
String.prototype.matchAll
Module namespace exports
BigInt: arbitrary-precision integers in JavaScript
Dynamic import()

If you were a user of JavaScript prior to ES2015 (ES3 / ES5. What happend to ES4?), then you've experienced a tremendous amount of progress in the language. One of the most important parts of the language, handling asynchronous execution, has undergone multiple iterations to finally arrive at something syntaically similar to other languages such as C#, Python, and so on.

JavaScript, over the past six years, has progressed from this:

function doSomething(someParams) { myAsynchronousFunc(someParams, function callback(data) { // Operate on data // Execution finishes inside callback }); }

To this:

async function doSomething(someParams) { const data = await myAsynchronousFunc(someParams); // Operate on data inside of original context // Execution finishes inside of original context }

A lot had to change under the hood of the language to get to this point (Promises, Generators) which has meant:

  • Codebases now have a visual age.
    • "Callbacks, eww"
    • "Hey, what's that star thing and yield do?"
  • Open source maintainers have had to keep up with modernizing their projects.
  • JavaScript developers have had to learn and relearn the language as new features enter the language.
  • No clear conventions exist on when to use new language features.

I'm not arguing against Promises, async/await, or any of the other new features that will change the way code is written (like optional chaining) but I think developing conventions around an ever-moving target is setup for failure. For example, I see arrow functions in Node.js code when they're not necessary for capturing the context of this. I almost never see asynchronous errors correctly handled one by one like you would be forced to do in Go. Oh, and a lot workplace bike-shedding.

Rubyists have been waiting for Ruby 3.0 for at least five years and Gophers for Go 2.0 since as early as 2017. This has given those communities time to internalize language changes, voice their opinions, and actually look forward to the next iteration. But, most importantly, developers in both communities know what good code looks and feels like. In 2020, I cannot say the same for JavaScript but I would really like to.

© Nick Olinger 2023