💾 Archived View for marc.huffnagle.net › blog › 2022-12-31-developer-beware.gmi captured on 2024-08-18 at 17:15:02. Gemini links have been rewritten to link to archived content
-=-=-=-=-=-=-
31 December, 2022
If I had to describe what it's like to work in JavaScript, I'd have to say, "it's like the cave scene in Raiders of the Lost Ark." The language is one booby trap after another. You can never let your guard down. Developers new to JavaScript don't realize that the language is out to get them, they write code that seems perfectly reasonable, and ... it fails. Developers who have used JavaScript for a long time know to watch their step and be very, very careful.
Say you take a string from the user and parse it into a number. Which function do you use?
If parsing fails, what will happen? The behavior is not intuitive, and slightly different depending on which function you use.
Number("0A") // => NaN parseInt("0A") // => 0
Should that "0A" value have actually returned 0? Was 0 really what the user intended, or was that a typo?
Being the conscientious developer you are, you check to make sure you didn't get NaN back.
if (value === NaN) throw new Error('Not a number');
Even when you specify an invalid input, though, that exception is never thrown. What's going on?
Reading the docs for NaN, you'll find that:
Or say you have a simple "increment" function, but you forget to parse the user's input into a number first.
const increment = (x) => x + 1; increment(2) // => 3, makes sense increment("2") // => "21" ???
Other examples:
The list goes on and on. These aren't hypotheticals. I've seen almost all of these issues in recent PRs that I've reviewed. It isn't the fault of the new (or even mid-level) JavaScript developer, they just haven't discovered how many "gotchas" there are in this language.
So, if you have to write JavaScript (and there are a lot of good reasons to do that), how do you do it safely?
There are two tools that are essential for writing safe JavaScript: TypeScript and ESLint. In combination, they can help you detect many potential bugs during development as opposed to runtime.
https://www.typescriptlang.org/
Using static types can help prevent a lot of subtle issues in JavaScript code. TypeScript forces you to think about those situations where a value might be undefined, but you forgot to check for it. Or, as in the "increment" example above, accidentally passing a string into a function that's expecting a number. It's also valuable "documentation that compiles", but that's an article for another day. I won't spend too much time on "why use TypeScript", there are a lot of good articles that cover that already.
If you're going to use TypeScript, the best option is to use it from the beginning of your project and write ".ts" files. You'll find that the code you write when you're being explicit about types is going to be different (and I'd argue better) than the code you'd write without static types.
On the other hand, if you have a large JavaScript codebase, then rewriting it in TypeScript may not be realistic. In that situation, using the TypeScript compiler to check your JavaScript code can still help you uncover a lot of bugs. The TypeScript Handbook has an excellent section on how to use TypeScript and JSDocs to type check your ".js" code. Be prepared, though. It's going to find a lot.
Intro to TypeScript in JavaScript
The other essential tool for writing safe JavaScript is ESLint. ESLint is commonly used for enforcing a uniform code style, but what I'm more interested in is its ability to detect bugs as they're written.
ESLint has a ton of built-in rules. Reading through them is a good exercise, because they point out many of the traps that developers can fall into. The documentation for each rule also shows examples of "good" and "bad" code. The "possible problems" rules are probably the most important for code correctness.
In addition to the built-in rules, there are a lot of ESLint plugins that provide additional checks. A few that you should take a look at are:
eslint-plugin-unicorn - More than 100 powerful ESLint rules
eslint-plugin-jest - ESLint rules for Jest
eslint-plugin-promise - Enforce best practices for JavaScript promises
eslint-plugin-n - Additional ESLint rules for Node.js
eslint-plugin-jsdoc - JSDoc linting rules for ESLint
eslint-plugin-security - ESLint rules for Node Security
eslint-plugin-eslint-comments - Additional ESLint rules for ESLint directive comments
eslint-plugin-lodash - Lodash-specific linting rules for ESLint
Every project is going to have a slightly different ruleset, but I highly recommend creating a robust ESLint configuration and using it to detect as much as possible.
Most editors make it easy to add TypeScript and ESLint seamlessly into your workflow. VSCode has excellent TypeScript support, as do most other editors that support the Language Server Protocol.
For ESLint, there is the ESLint extension. It monitors your code as you type and highlights potential issues detected by ESLint. I'd suggest enabling the "editor.formatOnSave" option as well.
TypeScript and ESLint should be part of your CI workflow. Adding ESLint is easy, just run "eslint" in one of your test steps. TypeScript checking is simple as well using the "--noEmit" flag. That will check your code and report on any type errors, but will not write any compiled JS to the filesystem. Both ESLint and TypeScript should be blocking steps in your build pipeline, similar to the automated tests.
When you first add TypeScript and ESLint to existing codebases, you are going to get a lot of errors reported. I've seen over 10,000 reported errors on some larger projects. It can be unrealistic to fix all of them at once, and doing so would also create a high risk of regressions.
One option to help incrementally make your code better, without fixing everything at once, is to track the number of reported errors and only fail the build if the number of errors goes up. If the number of errors goes down, make that the new limit. To help with that, I've created two npm packages:
These two utilities will "ratchet down" the number of errors allowed so that, over time, your code will trend toward fewer errors.
I have written a lot of JavaScript code over the years, from very small libraries to extremely large distributed systems handling petabytes of data. Even with all of that experience, I am very uncomfortable when I move onto a project that is using JavaScript but does not have TypeScript and ESLint as backstops. That is the first thing I put in place, and it's often very revealing. JavaScript can be used for creating very robust and bug-free systems, but not on its own. Tread carefully, and use the tools available to keep yourself out of trouble.