Updated at April 18, 2020
— 11 min read
Remark: The following advice and techniques are valid for many languages, not only Javascript! Also be aware that the upcoming information is opinionated.
Update (19.08.2018): Improved the code by using Map
I held a code review on a clients project recently, as usual when the team has been implemented a new feature. The code was ‘working’, but far from optimal because a naive approach has been taken. This is totally fine, as reviews are for such situations. But after writing code I suggest to look over it once again and to refactor it on the fly (after the tests are passing).
Let me outline the context for the code:
Think about taking a course or participating in a seminar. After people have signed up for such a seminar, they receive an email with a confirmation link. After confirming that they are participating, they get a confirmation mail with a link to cancel the participation. Both links will call a website, which calls then an API with the submitted tokens. The response of this API call is the condition for the text that will be displayed on the website. Then our function comes in. It will return the text shown depending on the API response. Also, it is a pure function (no side-effects). Catching request errors has been done before.
Now here goes the code. Try to read over it just once:
const evaluateResponseOld = response => {
let text = ''
if (response.valid) {
if (response.type === 'Confirmation') {
if (response.used) {
text = 'This token has already been used'
} else {
text = 'Thanks for your subscription'
}
} else if (response.type === 'Cancellation') {
if (response.used) {
text = 'This token has already been used'
} else {
text = 'You have been unsubscribed successfully'
}
}
} else {
text = 'The provided code is invalid'
}
return text
}
Were you able to find out all the different cases and returns after one read? I wasn’t back then, though I read code every day.
So, as you likely have recognized, the code is unnecessarily complex and has, therefore, a high cognitive load. You always have to remember which condition was fulfilled. Also, the
Now imagine that an additional state must be handled because the API will respond to another scenario now (for example
Code duplication per se is better than a wrong abstraction but in this case the code is unnecessarily duplicated and therefore more error-prone. Think about you have to change
So… how can we do better? We will look into some techniques I use to transform this code in a more readable snippet. It only takes six small iterations!
So, why should we remove temporary variables from the code (as far as possible)? Well, first of all, it reduces cognitive load. When almost all variables are immutable (or no variables exist), you don’t have to think about their state! In our scenario, we can even omit the
const stepOne = response => {
if (response.valid) {
if (response.type === 'Confirmation') {
if (response.used) {
return 'This token has already been used'
} else {
return 'Thanks for your subscription'
}
} else if (response.type === 'Cancellation') {
if (response.used) {
return 'This token has already been used'
} else {
return 'You have been unsubscribed successfully'
}
}
} else {
return 'The provided code is invalid'
}
}
Guarding
const stepTwo = response => {
if (!response.valid) {
return 'The provided code is invalid'
} else {
if (response.type === 'Confirmation') {
if (response.used) {
return 'This token has already been used'
} else {
return 'Thanks for your subscription'
}
} else if (response.type === 'Cancellation') {
if (response.used) {
return 'This token has already been used'
} else {
return 'You have been unsubscribed successfully'
}
}
}
}
Okay, this might be a bit controversial but I usually consider
Normally I’d replace all
const stepThree = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.type === 'Confirmation') {
if (response.used) {
return 'This token has already been used'
}
return 'Thanks for your subscription'
} else if (response.type === 'Cancellation') {
if (response.used) {
return 'This token has already been used'
}
return 'You have been unsubscribed successfully'
}
}
Now we should focus on the “leftovers”. As the code is more readable we can take a look and find possible duplicates in the code branches.
If we look over our transformed code we now see clearly that the
const stepFour = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
if (response.type === 'Confirmation') {
return 'Thanks for your subscription'
} else if (response.type === 'Cancellation') {
return 'You have been unsubscribed successfully'
}
}
One criterion was that we should be able to add new response states easily. To do this, we should minimize the code we would need for it. Currently, we’d have to add another
To reduce the changes when adding a
We will create a
const stepFive = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
const responseLookup = {
Confirmation: 'Thanks for your subscription',
Cancellation: 'You have been unsubscribed successfully'
}
if (responseLookup.hasOwnProperty(response.type)) {
return responseLookup[response.type]
}
}
You could also use a
const stepFiveWithMap = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
const responseMap = new Map([
['Confirmation','Thanks for your subscription'],
['Cancellation','You have been unsubscribed successfully']
])
if (responseMap.has(response.type)) {
return responseMap.get(response.type)
}
}
Placing the lookup object outside would also be viable as long as the lookup object don’t change through the function. This makes your function impure though, because it’ll then depend on an outside object and not only on it’s inputs.
To solve this, you could take both parameters (the
const responseMap = new Map([
['Confirmation', 'Thanks for your subscription'],
['Cancellation', 'You have been unsubscribed successfully']
])
const higherOrderStepFiveWithMap = lookupMap => response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
if (lookupMap.has(response.type)) {
return lookupMap.get(response.type)
}
}
const stepFiveWithMap = higherOrderStepFiveWithMap(responseMap)
// Now use stepFiveWithMap(yourResponseObject)
While I’d prefer this version because of the reusability, purity and low coupling, let’s stick to the simple and impure map version:
const lookupMap = new Map([
['Confirmation', 'Thanks for your subscription'],
['Cancellation', 'You have been unsubscribed successfully']
])
const simpleAndImpureStepFiveWithMap = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
if (lookupMap.has(response.type)) {
return lookupMap.get(response.type)
}
}
Alright! The code looks readable, has no duplication and is easily maintainable. One thing is missing though: Handling invalid states. Even if you think that this will never ever happen, it won’t hurt to throw an error if it does. In the best case, your monitoring/reporting tool will pick it up and tell you. It’s worth to write tests for invalid states as well, especially when it comes to user input!
const lookupMap = new Map([
['Confirmation', 'Thanks for your subscription'],
['Cancellation', 'You have been unsubscribed successfully']
])
const simpleAndImpureStepFiveWithMap = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
if (lookupMap.has(response.type)) {
return lookupMap.get(response.type)
}
throw new Error('Invalid state while evaluating response')
/*
we could also 'shorten' that a little with short-circuit:
const invalid = () => { throw new Error('Invalid state while evaluating response') }
return lookupMap.get(response.type) || invalid()
*/
}
We did it! Before wrapping everything up, let’s compare the initial and the final code:
Initial code
const evaluateResponseOld = response => {
let text = ''
if (response.valid) {
if (response.type === 'Confirmation') {
if (response.used) {
text = 'This token has already been used'
} else {
text = 'Thanks for your subscription'
}
} else if (response.type === 'Cancellation') {
if (response.used) {
text = 'This token has already been used'
} else {
text = 'You have been unsubscribed successfully'
}
}
} else {
text = 'The provided code is invalid'
}
return text
}
Final code
const lookupMap = new Map([
['Confirmation', 'Thanks for your subscription'],
['Cancellation', 'You have been unsubscribed successfully']
])
const simpleAndImpureStepFiveWithMap = response => {
if (!response.valid) {
return 'The provided code is invalid'
}
if (response.used) {
return 'This token has already been used'
}
if (lookupMap.has(response.type)) {
return lookupMap.get(response.type)
}
throw new Error('Invalid state while evaluating response')
}
As you see, the final code is has a maximum depth of one (previously four), only three branches (previously five) and is even shorter than the initial one (five source lines of code less). The cyclomatic complexity, which measures the amount of independent paths the function can take on execution, decreased by two.
The techniques utilized in a nutshell:
I hope you liked the content! If so it would be neat if you could spread the word (f.ex. with the buttons below).
Questions left? Critics? What is your opinion on those techniques?
Hit me up on Twitter (@TheAlexLichter) or write me a mail (blog at lichter dot io).

I'm Alex, a German web engineering consultant and content creator. Helping companies with my experience in TypeScript, Vue.js, and Nuxt.js is my daily business.
More about meWith the release of Vue 3, a lot of things became easier. Unfortunately, loading dynamic assets such as images is not one of them. In the following article, I want to demystify the process of dynamic asset loading in Vue 3 or Nuxt 3 and explain why static assets can be loaded easily...
As a Vue developer, you may have heard the term server-side rendering. Even if you are not using a framework like Nuxt.js it is important to know how to write universal SSR-ready components. And guess what, it isn't even hard!