I am a Never Nester

Feb 17, 2023
header image

As someone who enjoys consuming endless programming related content on YouTube, I recently stumbled upon a video that truly caught my attention. The video is entitled “Why You Shouldn't Nest Your Code” by CodeAesthetic. In this article I will summarize what I learned from the video and I will share a few examples related to the topic from my professional career.

What is code nesting

Code nesting is a common practice when writing code. Every if statement, for loop, switch statement or call back function, adds depth which eventually leads to deeply nested blocks of code.

Here’s an example:

function processData(data: any[]) { for (let i = 0; i < data.length; i++) { if (data[i].type === "A") { for (let j = 0; j < data[i].values.length; j++) { if (data[i].values[j].isValid) { for (let k = 0; k < data[i].values[j].details.length; k++) { if (data[i].values[j].details[k].isImportant) { console.log(data[i].values[j].details[k].info); } } } } } else if (data[i].type === "B") { // do something else } } }

This code has two major problems:

1- It is deeply nested, with three levels of for loops which makes it difficult to read and understand.

2- if the structure of the data array changes, this code may no longer work as expected.

The drawbacks of code nesting

Nested code is much harder to read and understand. As code grows in complexity, it becomes increasingly difficult to keep track of what's going on which makes easy to miss important details. This can lead to bugs and other issues that are much harder to diagnose and fix.

Also, nested code can be difficult to maintain. As the code base grows, it becomes more challenging to add new features which can lead to longer development times and frustrated coworkers.

Be a Never Nester

As stated in the video, a never nester is a programmer who only tolerates three levels of depth. Anything beyond is not allowed and calls for refactoring.

Here’s an example of three layers of depth:

function processData(data: any[]) // 1 { if(data.length > 0) // 2 { for (let k = 0; k < 3; k++) // 3 { console.log(`i: ${i}, j: ${j}, k: ${k}`); } } }

In order to respect the “Never Nester” philosophy while still writing complex algorithms, there are a few techniques that come in handy, extraction, early return, data structures and external libraries.

Extraction

As the name implies, extraction simply means refactoring parts of code into separate functions each with its own concern.

Example:

function processData(data: any[]) { data.forEach((item) => { if (item.type === "A") { item.values.forEach((value) => { if (value.isValid) { console.log(`${value.id}, ${value.name}, ${value.description}`); } }); } else if (item.type === "B") { // do something else } }); } // The previous deeply nested function can be extracted as such: function processData(data: any[]) { data.forEach((item) => { processItem(item); }); } function processItem(item: any) { if (item.type === "A") { processValues(item.values); } else if (item.type === "B") { // do something else } } function processValues(values: any[]) { values.forEach((value) => { if (value.isValid) { console.log(`${value.id}, ${value.name}, ${value.description}`); } }); }

Early return

The early return pattern can be used to return from a function as soon as a certain condition is met. This can help avoid deeply nested blocks of code by breaking out of a function early.

For example:

function processData(data: any[]) { data.forEach((item) => { if(item.type === "A") { if (item.values.length > 0) { item.values.forEach((value) => { if (value.isValid) { console.log(value.detail.info); } }); } } }); } // The previous deeply nested function can be refactored using early return as such: function processData(data: any[]) { data.forEach((item) => { if (item.type !== "A") { return; } if (item.values.length == 0) { return; } item.values.forEach((value) => { if (!value.isValid) { return; } console.log(value.detail.info); }); }); }

Data structures

Data structures such as arrays, maps, or sets can be used to store data, and simple loops can be used to iterate through the data, rather than deeply nested blocks of code.

Here’s a very basic example:

for (let i = 0; i < 5; i++) { for (let j = 0; j < 5; j++) { for (let k = 0; k < 5; k++) { console.log(`k: ${k}`); } } } // instead of nesting loops, the following array can be used: const data = [0,1,0,1,0,1,0,1]; data.forEach((value) => { console.log(value); });

External libraries

Sometimes the best way to avoid deeply nested code is to not write any code at all, the best way to do this is by using external libraries to efficiently get the results needed.

Here’s an example:

const data = [3, 2, 5, 1, 4]; for (let i = 0; i < data.length - 1; i++) { for (let j = 0; j < data.length - i - 1; j++) { if (data[j] > data[j + 1]) { [data[j], data[j + 1]] = [data[j + 1], data[j]]; } } } console.log(data); // this unnecessarily nested block of code does the same as the following const data = [3, 2, 5, 1, 4]; const sortedData = _.sortBy(data); // https://lodash.com/ console.log(sortedData);

Observable Nesting in RXJS

From personal experience, the most frequent use of deep nesting in Angular occurs when working with observables.

When implementing complex algorithms in RxJS it is easy to create multiple layers of nested observables and operations, each one dependent on the previous one. This can result in complex and hard-to-understand code.

Never nest pipes and subscriptions

When working with observables, it is crucial to avoid nesting pipes and subscriptions. This is a clear indication of flawed observable logic. In most cases, It can easily be corrected by making use of appropriate RxJS operators or by breaking down the source observable into smaller observables, each with a specific purpose.

Example:

this.courseService .getCourse(lesson.id) .pipe( switchMap((course) => combineLatest([ of(course), from( this.getAdditionalDetails(course.id) ), this.progressService.getProgress( course.id, lesson.id ) ]).pipe( take(1), tap(([course, details, progress]) => { //...do some business logic with the results }) ) ), take(1) ).subscribe(); // This Observable can be refactored into the following const course$ = this.courseService.getCourse(lesson.id); const additionalDetails$ = this.course$.pipe( switchMap((course) => from(this.getAdditionalDetails(course.id))) ) const progress$ = this.course$.pipe( switchMap((course) => this.progressService.getProgress(course.id,lesson.id)) ) combineLatest([course$, additionalDetails$, progress$]).pipe( tap(([course, additionalDetails, progress]) => //...do some business logic with the results ) ).subscribe()

By respecting the maximum depth rule and using the techniques mentioned above, complex RXJS operator chains can be simplified into code that is easy to follow.

Takeaway

While nesting code may seem like a convenient solution in the short term, it's important to consider the long-term consequences. I hope that the techniques mentioned in this article and the video help ensure that your code is easy to read, efficient, and maintainable.

Be a Never Nester!