5 Anti-Patterns in JavaScript to Avoid When Working With Collections

Christopher T.

December 5th, 2019

Share This Post

Working with collections in JavaScript can become an appalling task especially when there is a lot going on in a function block.

Have you ever wondered how some projects in code look much nicer than others? Or when a seemingly difficult project ends up being so small your mind just goes off in a wild ride wondering just how they were able to keep it simple and robust at the same time?

When a project is easy to read while maintaining good performance you can be ensured that there's likely pretty good practices applied to the code.

It can easily become the contrary when code is written like a mess. At this point it's easy to get in a situation where modifying small bits of code ends up causing catastrophic problems to your application--in other words an error thrown that crashes a web page from continuing further. When iterating over collections it can become scary to watch bad code run.

Enforcing better practices is about inhibiting yourself from taking short directions which in turn helps to secure guarantees. This means that it depends on you to make your code as maintainable as possible in the long run.

This article will go over 5 anti-patterns to avoid when working with collections in JavaScript

A lot of the code examples in this article will embody a programming paradigm called functional programming. Functional programming, as Eric Elliot explains it, "is the process of building software by composing pure functions, avoiding shared state, mutable data, and side-effects.". We will often mention side effects and mutation in this post.

Here are ___ Anti-Patterns in JavaScript to Avoid When Working With Collections:

1. Prematurely passing functions as direct arguments

The first anti-pattern that we will be going over is prematurely passing functions as a direct argument to array methods that loop over collections.

Here is a simple example of that:

function add(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  if (callback) {
    callback(result)
  }
}

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(add)

So why's this an anti-pattern?

Most developers especially those who are more into functional programming may find this to be clean, concise and performant at its best. I mean, just look at it. Instead of having to do this:

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(function(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  if (callback) {
    callback(result)
  }
})

It's seemingly much nicer to just throw in the name of the function and call it a day:

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(add)

In a perfect world, this would be the perfect solution to work with all of our functions in JavaScript without ever having to break a sweat.

But it turns out that prematurely passing your handlers this way can cause unexpected errors. For example, lets go ahead and look back into our previous example:

function add(nums, callback) {
  const result = nums[0] + nums[1]
  console.log(result)
  if (callback) {
    callback(result)
  }
}

const numbers = [[1, 2], [2, 2], [18, 1], [4, 5], [8, 9], [0, 0]]

numbers.forEach(add)

Our add function expects an array where the first and second indexes are numbers and adds them and checks if there is a callback, invoking it if it exists. The problem here is that callback could end up being invoked as a number and will result in an error:

add function error crash

2. Relying on the ordering of iterator functions like .map and .filter

JavaScript's basic functions process elements in collections in the order they're currently at in the array. However, your code should not depend on this.

First, the ordering of iteration is never 100% stable in every language nor in every library. It's a good practice to treat every iteratee function as if they are run concurrently in multiple processes.

I've seen code that do something like this:

let count = 0

frogs.forEach((frog) => {
  if (count === frogs.length - 1) {
    window.alert(
      `You have reached the last frog. There a total of ${count} frogs`,
    )
  }
  count++
})

In most situations this is perfectly fine, but if we look closely it's not the safest approach to take as anything in the global scope can update count. If this happens and count ends up being decremented accidentally somewhere in the code, then window.alert will never be able to run!

It can get even worse when working in asynchronous operations:

function someAsyncFunc(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timeout)
  })
}

const promises = [someAsyncFunc, someAsyncFunc, someAsyncFunc, someAsyncFunc]

let count = 0
promises.forEach((promise) => {
  count++
  promise(count).then(() => {
    console.log(count)
  })
})

The result:

promises in count

Those of you who are more experienced in JavaScript will probably know why we get four number 4's logged to the console and not 1, 2, 3, 4. The point is that it's a better to use the second argument (commonly referred to as the current index) that most functions receive when iterating over collections to avoid concurrency:

promises.forEach((promise, index) => {
  promise(index).then(() => {
    console.log(index)
  })
})

The result:

promises in native index

3. Optimizing Prematurely

When you're looking to optimize what usually comes in between is your decision in choosing whether to prefer readability or speed. Sometimes it can become really tempting to put more attention to optimizing your app's speed instead of improving the readability of your code. After all, it's a widely accepted truth that speed in websites matter. But this is actually a bad practice.

For one, collections in JavaScript are usually smaller than you'd think, and the time it takes it to process every operation is also faster than you'd think as well. A good rule to follow here is that unless you know something is going to be slow, don't try to make it faster. This is called Premature Optimization, or in other words, attempting to optimize code that is possibly already most optimal in speed.

As Donald Knuth puts it, "The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.".

In a lot of situations it's easier to apply some better speed where the code ends up being a little slower than it is having to stress out maintaining a fast working code in a tangled mess.

I recommend to prefer readability, and then proceeding to measure. If you use a profiler and it reports a bottleneck in your application, optimize that bit only because now you know its actually a slow code, as opposed to attempting to optimize code where you think it could be slow.

4. Relying on state

State is a very important concept in programming because it is a concept that enables us to build robust applications but it can also break our applications if we don't watch ourselves enough.

Here is an example of an anti-pattern when working with state in collections:

let toadsCount = 0

frogs.forEach((frog) => {
  if (frog.skin === 'dry') {
    toadsCount++
  }
})

This is an example of a side effect, something definitely to watch out for as it can cause problems like:

  • Producing unexpected side effects (Really dangerous!)
  • Increasing memory usage
  • Reducing your app's performance
  • Making your code harder to read/understand
  • Making it harder to test your code

So what's a better way to write this without causing a side effect? Or how can we rewrite this using a better practice?

When working with collections and we need to work with state during the operation, remember that we can utilize certain methods that provide you with a fresh new reference of something (like objects).

An example is using the .reduce method:

const toadsCount = frogs.reduce((accumulator, frog) => {
  if (newFrog.skin === 'dry') {
    accumulator++
  }
  return accumulator
}, 0)

So what's happening here is that we're interacting with some state inside its block but we also utilize the second argument to .reduce where the value can be newly created upon initialization. This is using a better approach than the previous snippet because we're not mutating anything outside of the scope. This makes our toadsCount an example of working with immutable collections and avoiding side effects.

5. Mutating Arguments

To mutate something means to change in form or in nature. This is an important concept to pay close attention to in JavaScript especially in the context of functional programming. Something that is mutable can be changed while something that is immutable cannot (or should not) be changed.

Here's an example:

const frogs = [
  { name: 'tony', isToad: false },
  { name: 'bobby', isToad: true },
  { name: 'lisa', isToad: false },
  { name: 'sally', isToad: true },
]

const toToads = frogs.map((frog) => {
  if (!frog.isToad) {
    frog.isToad = true
  }
  return frog
})

We're expecting the value of toToads to return a new array of frogs that were all converted to toads by flipping their isToad property to true.

But this is where it becomes a little chilling: When we mutated some of the frog objects by doing this: frog.isToad = true, we also unintentionally mutated them inside the frogs array!

We can see that frogs are now all toads because it was mutated:

mutating collections

This happens because objects in JavaScript are all passed by references! What if we assigned the same object around in 10 different places in code?

If we for example were assigning this reference to 10 different variables throughout our code, then mutated variable 7 at some point later in the code, all of the other variables that hold a reference to this same pointer in memory will also be mutated:

const bobby = {
  name: 'bobby',
  age: 15,
  gender: 'male',
}

function stepOneYearIntoFuture(person) {
  person.age++
  return person
}

const doppleGanger = bobby
const doppleGanger2 = bobby
const doppleGanger3 = bobby
const doppleGanger4 = bobby
const doppleGanger5 = bobby
const doppleGanger6 = bobby
const doppleGanger7 = bobby
const doppleGanger8 = bobby
const doppleGanger9 = bobby
const doppleGanger10 = bobby

stepOneYearIntoFuture(doppleGanger7)

console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)

doppleGanger5.age = 3

console.log(doppleGanger)
console.log(doppleGanger2)
console.log(doppleGanger4)
console.log(doppleGanger7)
console.log(doppleGanger10)

Result:

mutating arguments bobby age

What we can do instead is to create new references each time we want to mutate them:

const doppleGanger = { ...bobby }
const doppleGanger2 = { ...bobby }
const doppleGanger3 = { ...bobby }
const doppleGanger4 = { ...bobby }
const doppleGanger5 = { ...bobby }
const doppleGanger6 = { ...bobby }
const doppleGanger7 = { ...bobby }
const doppleGanger8 = { ...bobby }
const doppleGanger9 = { ...bobby }
const doppleGanger10 = { ...bobby }

Result:

mutating arguments bobby age immutable

Conclusion

And that concludes the end of this post! I found you found this to be valuable and look out for more in the future!


Tags

javascript
best practice
anti pattern
composition
premature optimization

Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2021