Cleaner Code — The Structure (Code Nesting)

As a seasoned programmer, I always like readable code, not to discount the performance.
And to have a readable code — one of the most important thing is the structure of the code.
This might sound odd at first, but it has transformed the way I write and structure my code. The first and foremost important aspect of code structure is code nesting. You might be wondering, what exactly is code nesting? Let’s dive into this concept, its benefits, and how you can apply it to your coding practices with some concrete examples.
What is a Code Nesting?
Code nesting refers to the practice of placing one or more control structures (like loops, conditionals, or functions) inside another control structure. The more nesting you have in your code, the more difficult it can be to read and maintain.
For example, if you have a function like the one below, it is 1 level nesting.
function calculate(value1: number, value2: number): number {
return value1 + value2;
}
But if you add an if-condition, it will become 2 levels nesting e.g.
function calculate(values: number[]): number {
let result: number;
if(values.length == 1) {
result = values[0];
}
else if(values.length == 2){
result = values[0] + values[1]
}
return result;
}
Now let’s take it a bit further and add a for-loop. Now it becomes 3 levels nesting
function calculate(values: number[]): number {
let result: number;
if(values != null) {
for(value in values) {
result += value;
}
}
return result;
}
Each level of nesting adds a layer of complexity, requiring the programmer to keep track of multiple contexts simultaneously.
A function that goes beyond three levels of nesting starts to become problematic.
The Problem with Deep Nesting
Deeply nested code can be hard to follow. Each additional level of nesting increases the cognitive load on the programmer, making it more difficult to understand the logic at a glance. For example, consider the following deeply nested function:
function processData(data: Item[]): void {
for (const item of data) {
if (item.isValid()) {
if (item.needsProcessing()) {
processItem(item);
if (item.requiresNotification()) {
notifyUser(item);
}
else {
logItem(item);
}
}
}
else {
discardItem(item);
}
}
}
This function is challenging to read quickly. Let’s look at how we can improve it using two key strategies: extraction and inversion.
Method 1: Extraction
Extraction involves pulling out parts of the function into their own dedicated functions. This reduces the complexity of the original function and improves readability.
function processData(data: Item[]): void {
for (const item of data) {
if (item.isValid()) {
processValidItem(item);
}
else {
discardItem(item);
}
}
}
function processValidItem(item: Item): void {
if (item.needsProcessing()) {
processItem(item);
handleNotification(item);
}
}
function handleNotification(item: Item): void {
if (item.requiresNotification()) {
notifyUser(item);
}
else {
logItem(item);
}
}
Method 2: Inversion
Inversion is about flipping conditions and using early returns to avoid deep nesting. This approach helps keep the code flat and easier to follow.
function processData(data: Item[]): void {
for (const item of data) {
if (!item.isValid()) {
discardItem(item);
continue;
}
if (!item.needsProcessing()) {
continue;
}
processItem(item);
if (item.requiresNotification()) {
notifyUser(item);
}
else {
logItem(item);
}
}
}
A Practical Example: Download Manager
Let’s take a more complex example — a download manager that handles multiple downloads simultaneously. Here’s the initial code:
function runDownloadManager(downloads: Download[]): void {
for (const download of downloads) {
if (download.state == 'Pending') {
startDownload(download);
}
else if (download.state == 'InProgress') {
const result = processDownload(download);
if (result == 'Complete') {
markComplete(download);
}
else if (result == 'Error') {
if (download.retryCount < MAX_RETRIES) {
retryDownload(download);
}
else {
failDownload(download);
}
}
}
else if (download.state == 'Complete') {
finalizeDownload(download);
}
}
}
This code is functional but can quickly become unwieldy. Applying extraction and inversion can make it much cleaner:
function runDownloadManager(downloads: Download[]): void {
processPendingDownloads(downloads.filter(d => d.state == 'Pending'));
processInProgressDownloads(downloads.filter(d => d.state == 'InProgress'));
finalizeCompletedDownloads(downloads.filter(d => d.state == 'Complete'));
}
function processPendingDownloads(pendingDownloads: Download[]): void {
for (const download of pendingDownloads) {
startDownload(download);
}
}
function processInProgressDownloads(inprogressDownloads: Download[]): void {
for (const download of inprogressDownloads) {
handleInProgressDownload(download);
}
}
function finalizeCompletedDownloads(completedDownloads: Download[]): void {
for (const download of completedDownloads) {
finalizeDownload(download);
}
}
function handleInProgressDownload(download: Download): void {
const result = processDownload(download);
if (result == 'Complete') {
markComplete(download);
}
else if (result == 'Error') {
handleDownloadError(download);
}
}
function handleDownloadError(download: Download): void {
if (download.retryCount < MAX_RETRIES) {
retryDownload(download);
}
else {
failDownload(download);
}
}
Conclusion
Adopting this philosophy encourages writing cleaner, more maintainable code by limiting the levels of nesting. By using extraction and inversion, you can reduce complexity and improve readability, making your code easier to understand and maintain. As a bonus, you’ll likely find that your code’s quality improves, and debugging becomes less of a headache. So, give it a try — your future self will thank you!