Clean JavaScript: Avoiding Boolean Parameters
Introduction
As we close out 2022, JavaScript remains a popular scripting language for browser-based applications and is maybe the go-to choice for new developers breaking into software development. It’s a relatively straightforward language and you can build some pretty cool stuff quickly. Unfortunately, its versatility can be a double-edged sword - JavaScript projects that start out fast often slow to a crawl as side-effects, ambiguous function definitions, and code duplication make new changes riskly and tedious.
The Clean JavaScript series will be a series of JavaScript coding practices to help new developers avoid writing spaghetti code. It may reference more advanced topics like Functional Programming or Big O Analysis, but I’ll do my best to provide examples and link to more articulate definitions where applicable.
This is the first article in the series, and intends to tackle all-to-common pitfall many JavaScript developers make - the boolean trap.
The Problem
The first time I heard of the Boolean Trap was in 2013 when a coworker shared this blog post during a code review. I was trying to extend an existing function on the webapp to emit an event after successfully retrieving some data in the server. But, because I didn’t know how the other parts of the application might be affected, I decided to add an optional parameter canDispatchEvent
.
async function updatePost (postId: string, changes: Partial<Post>, canDispatchEvent: boolean): Promise<Post> {
const response = await fetch(`https://example.com/post/${postId}`, {
method: 'POST',
body: JSON.stringify(changes)
});
const updatedPost = response.json();
if (canDispatchEvent) {
blogActionEmitter(Actions.PostUpdated, updatedPost);
}
return updatedPost;
}
Brilliant, right?!
At the time, it seems innocuous. The only place that passes this parameter is the code I just changed, and the rest of the application continues on in ignorance.
But let’s take a look at an example of how this method was used and why this new optional boolean param will lead us down the dark road of spaghetti hell.
// update-post-form.component.tsx
async function handleSavePost (postId, changes) {
...
await updatePost(postId, changes, true);
...
}
// post-table.component.tsx
async function handleUpdatePost (postId, changes) {
...
await updatePost(postId, changes);
...
}
async function handleDeletePost (postId) {
...
await updatePost(postId, { deleted: true });
...
}
You can see the method I’ve updated in handleSavePost
, and the merrily ignorant examples in the post-table component.
A little vague but since we just saw the updatePost method we can generally grok what’s going on.
Fast forward to 6 months from now. You’re hacking away on post-table and realize we DO want to emit an action when a post is updated, but not when it’s deleted. So we make the change:
async function handleUpdatePost (postId, changes) {
...
await updatePost(postId, changes, true);
...
}
async function handleDeletePost (postId) {
...
await updatePost(postId, { deleted: true }, false);
...
}
Wait.. erm.. which one does which again? Does true
mean dispatch the action, or false
to prevent the action from firing? Suddenly your vision is getting spotty and you need to sit down.
To quote the Robert “Uncle Bob” Martin:
the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code.
The problem with boolean arguments is they lack context. It forces you, and the developers coming after you, to be intimately aware of how updatePost
is implemented. Any time you touch that function you have to reload the entire context of updatePost
, and any methods it may call, back into memory and act accordingly.
Clearly this doesn’t scale. Pretty soon you’re 3 weeks behind on your assignment and using the last developer’s name as an expletive.
So we’re on the same page regarding WHY the boolean trap is a gentle path towards insanity. What’s the solution?
The Solution
There is a quick and dirty way to immediately inject additional context into the method signature and the places that call it: Convert the boolean argument into an object:
async function updatePost (postId: string, changes: Partial<Post>, options: { canDispatchEvent: boolean } = {}) {...}
// update-post-form.component.tsx
async function handleSave (postId, changes) {
...
await updatePost(postId, changes, { canDispatchEvent: true });
...
}
// post-table.component.tsx
async function handleUpdatePost (postId, changes) {
...
await updatePost(postId, changes, { canDispatchEvent: false });
...
}
...
async function handleDeletePost (postId) {
...
await updatePost(postId, { deleted: true }, { canDispatchEvent: true });
...
}
Suddenly, like a beam of light through a cloudy sky, we see what the boolean parameter is trying to do. Every time we invoke updatePost
we can make an educated decision about whether or not it should also dispatch an action. We don’t need to creep on updatePost
through the window or really care at all what how it’s implemented, and the developer can sleep soundly at night again.
An Aside
Although somewhat outside the context of this article, it is worth mentioning that boolean traps may sometimes be indicative of deeper problems in the design of your application.
In the example above, we force the callees of updatePost
to make the decision to also emit an event anytime we make the network call. What is the purpose of the event? What effects does that event trigger in the application? What will happen if I don’t emit the event?
Anything we can do to reduce the burden of retaining this context will make future you happier.
The solution to this problem is a bit trickier than changing a parameter type - it’s most likely application (and team?) specific. Maybe you create a new data structure to tether network calls to side effects, like the Redux/Saga pattern. Maybe it’s as simple as separating the behavior into separate functions - if the action should only be excluded when the post is deleted, for instance, than separating updatePost
(emits event) and deletePost
(no event) seems like a prudent solution.
Conclusion
I left the review meeting in 2013 with renewed vigor. I sought out and eliminated boolean traps with the cold ruthlessness of the Terminator. Sometimes, this refactor lead to new insights, like the data structures/patterns described above. After each commit, the code became more expressive, easier to grok, and the mental burden to make changes diminished over time.
Hopefully you now understand why boolean parameters are problematic and how a simple refactor can alleviate loads of mental burden. Future you will thank you!
As always, thanks for reading. - Ian