Part 1: Non-essential but non-trivial decisions

The first issue that’ll pop up as soon as you open your favorite IDE to start that fresh new project in NodeJS is the absurd amount of decisions that you’ll need to make that shouldn't even matter or need to be made. Javascript has the luxury of not only being both a server-side language and client-side language, but also offers a wide-array of ‘flavors’ to choose from based on its vibrant but fragmented open-source community.

Javascript is unique in the sense that the language itself varies from browser-to-browser, project-to-project, company-to-company. Although the inconsistencies are improving and converging there are still a plethora of non-standard choices to make before you even write a line of code:

Typed JS

Should you use Typescript or Flow for static type analysis? If so, which one, and if neither, why not? What is the expected ROI for using either of these technologies, or not using them? What will the maintenance cost be to maintain your type system, and how difficult will it be to get full coverage for an existing system? Is full coverage necessary? What about runtime type errors from API responses? If you type API data as a string but didn't account for it also potentially being an array will your code still break during runtime?

The benefits of statically-typed languages goes beyond assisting the developer. With static types you can allocate just the right amount of memory for each type, which adds some additional value to the tedium of definining and working with types. In a dynamic language like Javascript, however, most of the primitive types are dynamic and stored on the heap meaning there's no memory optimization that can occur by defining types.

If static type analysis was built-in and ready to use, and consistent across JS environments, I'd say the developer convenience would make for a worthwhile investment. However, getting Typescript or Flow to work in both NodeJS and browser environments by configuring build systems, adding plugins and linters and making sure all of the versions play nicely together can be a pain. These configurations need to be maintained as well as new versions are released, leading to an ongoing maintenance of your type system. Think about that, you need to maintain your type system.

Even if you still get a positive ROI out of Typescript, you're probably limiting it considering it's not a standard part of the language but an open-source bolt on that needs to play nicely with the rest of the fragile JS ecosystem. Positive gain will be tempered by upfront and ongoing maintenance costs.

Linting

Let's face it. Using double-quotes for strings in Javascript is basically a death wish, how could you ever go on with those little devils when single-quotes are clearly better?

Please, before we solve any of our pressing business challenges or even think about building a product we need to know for certain whether we should use semicolons or not?

You only have to choose from about 30 different linting presets and hundreds of different rules to override. And fixing lint issues will only lead to extensive merge conflicts in existing projects, or be ignored altogether by changing a rule from a lint error to lint warning.

Oh and make sure you have plugins for all of your language extensions like JSX.

Look, I'm not here to say you shouldn't lint your code, but come on now, this is absolutely absurd. The language is far too loose and trying to control that is a waste of time and energy (money).

Other languages, like Go, have linting/formatting built-in, with minimal configuration options because the language was well designed and may have different semantic reasons for things like single-quotes and double-quotes.

Just like with static type analysis, JS relies on non-standard, open-source solutions that need to be well-configured in order to play nicely with the other open-source solutions needed to patch the language. It takes a lot of effort to set up and maintain because it's not part of the standard language, which begs the same question as the previous section "What is the ROI?". Oops, I meant 'What is the ROI?'.

Dependencies

Oftentimes the vibrant community in the Javascript world is considered one of its biggest strengths. After all, the whole world is stuck with Javascript and therefore is highly incentivized to make it as bearable and efficient as possible. Personally, however, I think this is backwards.

Such a large, diverse open-source community has several real drawbacks, such as the ‘house of cards’ it takes to build a real application between plugins, poor adherence to semantic versioning, the overwhelming amount of options, the constant updates and incompatibilities, and extreme pace of innovation.

The JS community may look like a good thing on the surface, but in reality it’s a testament to the fact that JS was not designed well from the very beginning, and so now it takes this monumental worldwide effort to make it workable in today's world. Maybe if Typescript, Babel and Webpack and their myriad of plugins were unnecessary it’d be much less chaotic and easier to get started but setting all of that up and maintaining it is a significant effort, and keeping it consistent across an organization is near impossible.

With that said, there are some absolute brilliant people working in this community throughout the world that are way smarter than me. They've been responsible for some excellent transformations in the front-end world for years, and without them I wouldn't have a leg to stand on. I am grateful for all of their hard work, but can't help but ask what they'd be capable of if they invested that time into something already designed well and technically sound? This tremendous effort and investment into Javascript comes with a real opportunity cost.

How much further would we be if the effort making JS something it's not was spent elsewhere?

Testing

I have another article planned on the subject of testing, but for now all I’ll say is testing in JS suffers the same underlying issues as the rest of the items on this list; too many options. Too many test frameworks, helpers, and utilities. In addition, front-end virtual DOM testing and server/client differences increase the complexity, and when you consider all of the possible issues a UI can have it gets even more complicated.

Again, I'll ask the same question as I do in the previous sections... what is the ROI on testing your Javascript applications once you factor in the true up-front and ongoing cost, as well as the sad fact that no matter how much testing you do it will never be enough?

Import vs require

Personally, I think the craziness around the simple task of importing modules is one of the worst issues in Javascript, and highly unique to the language. Between script tags, RequireJS, AMD, UMD, CommonJS, ES6 import, dynamic import, Michael Jackson script, tons of different file extensions (js, mjs, jsx, ts, tsx etc) and a huge variance of usage in the open-source community there is no simple way to import (or require?) files in Javascript. It is INSANE, what other language has this level of inconsistency in something as simple as importing code?

Eventually ES6 modules should be the norm, but how long will that take until every open-source project and every application reaches that level of consistency? This is yet another example of something that is a given, non-factor in other languages but a hot mess in Javascript, and really makes a difference when you start trying to do more complex things like code-splitting/dynamic imports.

Idiomatic

I didn't make it to the next round of an interview once after doing a JS code test for a company. I completed the challenge, but was docked points for not using 'idiomatic' Javascript (the example used ES2015 classes).

I'm glad I didn't get the job, because it is a prime example of why I don't want to work with JS. What even is idiomatic JS? If you're talking 'idiomatic' in terms of original design it would mean dynamic (no Typescript/Flow) code with prototypal inheritance (no ES2015 classes). To imply ES2015 classes are idiomatic means you don't understand them or Javascript, but nobody uses 'idiomatic' JS anyway.

The same project can, and oftentimes do, have a mix of both CommonJS & ES2015 modules, both ES2015 classes & prototypal inheritance, both dynamic & static types, both functional/immutable & imperative/mutable patterns and potentially even reactive/observable patterns, abuse of new and this, and any combination of generators, promises, callbacks, async/await and coroutines. That's idiomatic JS, the 'anything goes but nothing is correct' language.

On a side note, this is just another reason why hiring is broken, but that's another topic for another day (but will be coming soon).

Async

I know what you're thinking. These are all small, petty, insignificant reasons to not use a language, right? On their own, I'd agree, but when the whole language is like that, fragmented, hacked-together and inconsistent, there must be some upside to justify its quirks, right?

Perhaps there is. Or was. Without spoiling the next section I'd say at the time NodeJS emerged it did have some advantages over the competition, back in 2010 or so. I'm not arguing that it isn't a viable option over PHP, Ruby or even Java and Python. But it's not 2010, it's 2020, and picking NodeJS over some of the newer generation languages seems curious at best.

Javascript is often sold on its async capabilities, its famous "Async non-blocking I/O" shtick. The event loop may be an improvement to 2010 alternatives, but by default callbacks are used which are a mess to work with. Standardized Promises (sorry Bluebird) have helped tremendously, but the fun doesn’t stop there! Async/await is syntactic sugar around generators, a lower-level async utility, that improves the readability of Promise-based applications, but are still fairly new and don’t have full support across browsers and NodeJS versions.

The irony is that while Javascript tried its best to protect developers from the challenges and intricacies of asynchronous programming (threads, locks etc) it ended up creating a new mess of abstractions and work-arounds, and instead of JS developers learning proper multi-threaded programming techniques we ended up spending just as much time learning the new async pattern of the month.

Conclusion

NodeJS is like the death by a thousand cuts, which is what makes it so dangerous. None of the decisions you need to make are non-trivial, and require careful consideration upfront based on the size of the team, problem domain, project and technical challenges, but are more often than not things you get for free out-of-the-box with other modern languages.

It's almost as if we should've just let Javascript be Javascript, instead of the Frankenstein we have today. But then we wouldn't have all of the things that actually make Javascript workable. But then you ask "why do we even want to use a programming language that isn't useable in it's unaltered form? And what is even the upside of the language if we were to resolve those issues?".

If I'm looking for evidence of a toxic relationship I'd say there are plenty of red flags with NodeJS between the heavy investment up-front and the low upside/return.