DEV Community

Nozibul Islam
Nozibul Islam

Posted on • Edited on

The Art of Clean Code: A Practical Guide to Writing Maintainable JavaScript

The Art of Clean Code: A Practical Guide to Writing Maintainable JavaScript.

Introduction:

Writing clean code is more than an aesthetic choice—it's a fundamental practice that reduces bugs, enhances collaboration, and ensures long-term maintainability of software projects. This guide explores the principles, practices, and pragmatic approaches to writing clean JavaScript code.

Core Principles

1. Readability First

Code is read far more often than it's written. Good code tells a story that other developers (including your future self) can easily understand.

Bad:

const x = y + z / 3.14;
Enter fullscreen mode Exit fullscreen mode

Good:

const radius = diameter / Math.PI;
Enter fullscreen mode Exit fullscreen mode

2. Maintainability Matters

Maintainable code is modular, follows SOLID principles, and minimizes dependencies.

Bad:

function calculateArea(radius) {
    // ...lots of nested logic...
    // ...complex calculations...
    // ...multiple responsibilities...
    return result;
}
Enter fullscreen mode Exit fullscreen mode

Good:

function calculateArea(radius) {
    return Math.PI * radius * radius;
}
Enter fullscreen mode Exit fullscreen mode

3. Testability

Clean code is inherently testable. Break down complex operations into smaller, verifiable units.

Bad:

function getRandomNumber() {
    return Math.random();
}
Enter fullscreen mode Exit fullscreen mode

Good:

function getRandomNumber(randomGenerator = Math.random) {
    return randomGenerator();
}
Enter fullscreen mode Exit fullscreen mode

4. Scalability

Clean code grows gracefully with your project.

Bad:

function handleUserData(data) {
    if (data.type === 'admin') {
        // 50 lines of admin logic
    } else if (data.type === 'user') {
        // 50 lines of user logic
    } else if (data.type === 'guest') {
        // 50 lines of guest logic
    }
}
Enter fullscreen mode Exit fullscreen mode

Good:

const userHandlers = {
    admin: handleAdminData,
    user: handleUserData,
    guest: handleGuestData
};

function handleUserData(data) {
    return userHandlers[data.type](data);
}
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls and Solutions:

1. The Naming Dilemma

Names should reveal intent and context.
Bad:

function calc(a, b) {
    return a * b + TAX;
}
Enter fullscreen mode Exit fullscreen mode

Good:

function calculatePriceWithTax(basePrice, taxRate) {
    const TAX_MULTIPLIER = 1;
    return basePrice * taxRate + TAX_MULTIPLIER;
}
Enter fullscreen mode Exit fullscreen mode

2. Avoiding Callback Hell

Replace nested callbacks with modern async patterns.

Bad:

getUserData(userId, function(user) {
    getOrders(user.id, function(orders) {
        processOrders(orders, function(result) {
            // More nesting...
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Good:

async function processUserOrders(userId) {
    try {
        const user = await getUserData(userId);
        const orders = await getOrders(user.id);
        return await processOrders(orders);
    } catch (error) {
        handleError(error);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Managing Configuration

Establish a single source of truth for configuration values.

Bad:

// Scattered across multiple files
const API_KEY = 'abc123';
const API_ENDPOINT = 'https://api.example.com';
Enter fullscreen mode Exit fullscreen mode

Good:

// config.js
export const config = {
    api: {
        key: process.env.API_KEY,
        endpoint: process.env.API_ENDPOINT
    }
};
Enter fullscreen mode Exit fullscreen mode

Pragmatic Trade-offs:

Performance vs. Readability

Balance readability with performance needs:

// More readable, slightly less performant
const doubledNumbers = numbers.map(n => n * 2);

// Less readable, more performant (when performance is critical)
for (let i = 0; i < numbers.length; i++) numbers[i] *= 2;
Enter fullscreen mode Exit fullscreen mode

Pure Functions vs. Side Effects

While pure functions are ideal, real applications need side effects. Isolate and manage them carefully:

// Pure function
function calculateTotal(items) {
    return items.reduce((sum, item) => sum + item.price, 0);
}

// Necessary side effect, clearly isolated
async function saveOrderToDatabase(order) {
    await database.orders.save(order);
    logOrderCreation(order);
}
Enter fullscreen mode Exit fullscreen mode

Best Practices:

1. Use Meaningful Names

  • Variables should indicate their purpose
  • Functions should describe their action
  • Classes should represent their entity

2. Keep Functions Small

  • Each function should do one thing well
  • Aim for no more than 20 lines per function
  • Extract complex logic into separate functions

3. Avoid Magic Numbers

  • Use named constants for all numeric values
  • Group related constants in configuration objects

4. Handle Errors Gracefully

  • Use try/catch blocks appropriately
  • Provide meaningful error messages
  • Consider error recovery strategies

Conclusion:

Clean code is a journey, not a destination. While perfect cleanliness might be unattainable, striving for clean code through consistent practices and pragmatic trade-offs leads to more maintainable, reliable, and collaborative codebases. Remember that context matters—what's clean in one situation might not be in another. The key is finding the right balance for your specific needs while maintaining code that others (including your future self) will thank you for writing.

🔗 Connect with me on LinkedIn:

Let’s dive deeper into the world of software engineering together! I regularly share insights on JavaScript, TypeScript, Node.js, React, Next.js, data structures, algorithms, web development, and much more. Whether you're looking to enhance your skills or collaborate on exciting topics, I’d love to connect and grow with you.

Follow me: Nozibul Islam

Top comments (42)

Collapse
 
brense profile image
Rense Bakker
function getRandomNumber(randomGenerator = Math.random) {
    return randomGenerator();
}
Enter fullscreen mode Exit fullscreen mode

This is of course something you should never do... Function wrappers are evil. Just call Math.random() directly.

Maybe what you mean to say is something along the lines of: "use some form of dependency injection to improve testability."

Collapse
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

This was an example of creating something that is testable. In this case, he's using Math.random since it's a great example of non-deterministic code which is horrible for unit testing. You don't want unit tests that fail sometimes and pass others, nor do you want meaningless ones. I think it might have been more obvious if the function was doing more than simply wrapping Math.random, but beyond that, it was an excellent example of doing di well (although, I'd personally have probably put it into a special object literal that I'd have called di or something that would make it clear and obvious that this is what it's for).

It probably would have been even better to show an example with an import, as this is a great way to handle di with imports. The default is the import, but it can easily be overriden with the injection.

The only thing that might be even better would be to create a closure around it so you don't need to continuously reassign the value. Or, if you must use classes (not my recommendation in JS, actually), then use a singleton or similarly scoped class.

Collapse
 
wass_supp_6e189b8fb3b6d16 profile image
Wass Supp

Ok Very good - I was really struggling why he had chosen such a for lack of better language random function to simply be returned. Difficulty again I may be missing something that should IMO never be called good. Maybe a clearer explanation in the op. Logical but if you know its not an issue. Great work though

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

Look at my response to @brense, in that I suggest doing something in the function that handles a specific scenario that might occur with the random function.

If you have that, it would better reflect the reason for di (which I usually prefer over import mocking).

Thread Thread
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

The core purpose of Dependency Injection (DI) is to handle specific scenarios that might occur with random function generation. For instance, if a random number falls outside a desired range, you could re-generate it or handle exceptions. Such logic would demonstrate a more meaningful use of DI.

Thread Thread
 
brense profile image
Rense Bakker

If you need Math.random to return a specific number for one of your test cases, you just do:
Math.random = vi.fn(() => 0.1)

Dependency injection can be useful and can simplify testing in certain cases, but this is not one of them.

Collapse
 
brense profile image
Rense Bakker

Yes, that's why I added the second paragraph of what I thought they were trying to explain... Imho the code examples are really not clear or in this case just wrong...

I disagree with your statement to add unit test specific code to the function... If your function behaves different in unit tests, compared to production, the unit test is irrelevant.

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

I have to disagree with the irrelevant part.

There are plenty on times when you have to test that code that "could" happen in production is properly tested.

Let's imagine that the code that is using the random function does something in particular if the value returned is less than .01.

This is something that "could" happen in production.

If you test the Math.random result directly in tests, it may randomly produce that value approximately once every hundred times. That means your unit test will fail most of the time and pass sometimes. That's a useless test. You need to be able to pass in a scenario where the unit test can check that the function works correctly when the value produced by Math.random is less than .01 by explicitly giving it a value that is less than that. Similar patterns are required for testing time based methods and more. You can't have tests that do a particular thing on Monday only work one day of the week.

I think we can both agree, the provided example isn't as good because it doesn't showcase something like this and so, we're simply testing Math.random itself, which, I agree is useless to test. Also, if we're attempting to show that it actually is returning random values, we would likely need a different kind of test than a unit test.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

The primary goal of Dependency Injection (DI) is to make code more testable. For non-deterministic functions like Math.random(), injection allows for more predictable and repeatable tests. In the future, more advanced DI can be achieved using imports or closures, making the code more flexible and easier to test.

Collapse
 
jvnm_dev profile image
Jason Van Malder

What about mocks?

Thread Thread
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

Yes, mocks are another alternative, however, they don't have as much longevity.

Let me provide an example.

Node used to use common js, now it's primarily using esm. And we are starting to see other tools like Deno that work differently still.

If we used di like this, much less code would need changing when moving from one paradigm to another. Whereas if we were utilizing one of the many mocking solutions that mock the globals or requires/imports, we introduce way more variables. I think you still have to do this sometimes, and di does not necessarily preclude mocking, it mainly tries to get away from the more magic aspects of mocking that aren't always so obvious and that often rely more on the runtime being used.

Thread Thread
 
brense profile image
Rense Bakker

I think this all boils down to: the right tool for the job. Sometimes you want dependency injection so you can test functions that may act different depending on which context they are executed in.

function getDbClient(){
  // ... some logic to switch databases or clients based on env vars for example ...
  return { type: 'postgres' }
}

function doSomething(someParameter, injectDbClient = getDbClient){
  const dbClient = injectDbClient()
  if(dbClient.type === 'postgres'){
    return 'do sql stuff'
  } else {
    return 'do something else'
  }
}

// test the default code branch
it('should return sql stuff', () => {
  const result = doSomething('123')
  expect(result).toEqual('do sql stuff')
})

// test specifically what happens if we inject the mongodb client
it('should return something else', () => {
  const mockInjectMongoDbClient = () => ({ type: 'mongodb' })
  const result = doSomething('456', mockInjectMongoDbClient)
  expect(result).toEqual('do something else')
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Your comment is completely incorrect. This function is an excellent example of dependency injection.

function getRandomNumber(randomGenerator = Math.random) {
    return randomGenerator();
}
Enter fullscreen mode Exit fullscreen mode

Advantages of this approach:

  • Highly beneficial for testing
  • Allows passing mock random generators during tests
  • Makes the function more flexible
  • Simplifies function testing

Example:

// During testing
function fixedRandomGenerator() {
    return 0.5;
}

const result = getRandomNumber(fixedRandomGenerator);
Enter fullscreen mode Exit fullscreen mode

This is an efficient design pattern that promotes:

  • Testability
  • Modularity
  • Separation of concerns

The default parameter Math.random ensures normal operation, while allowing custom generators when needed - a standard dependency injection technique in JavaScript.

Collapse
 
brense profile image
Rense Bakker • Edited
// Using dependency injection to create a fake code branch.
it('should return 1', () => {
  const mockRandomNumberGenerator = vi.fn(() => 1)
  const result = getRandomNumber(mockRandomNumberGenerator)
  expect(result).toBe(1)
})

// No functional difference by just mocking Math.random directly.
it('should return 1', () => {
  Math.random = vi.fn(() => 1)
  const result = getRandomNumber()
  expect(result).toBe(1)
})
Enter fullscreen mode Exit fullscreen mode

Both these tests are nonsense because getRandomNumber() doesnt do anything except wrap Math.random… Math.random is already properly tested by the EcmaScript people.

Yes you can use dependency injection for testing where it makes sense. However, your example is poorly chosen and doesnt make sense 🤷 as per my original comment: you should never do what you did, but perhaps you meant to say that you can use dependency injection to simplify testing in certain cases.

Collapse
 
armando_ota_c7226077d1236 profile image
Armando Ota

I agree .. all this hidden "magic" is so fubared after you come back on a project after a while ...

Collapse
 
thomasbnt profile image
Thomas Bnt

Hello ! Don't hesitate to put colors on your codeblock like this example for have to have a better understanding of your code 😎

console.log('Hello world!');
Enter fullscreen mode Exit fullscreen mode

Example of how to add colors and syntax in codeblocks

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

wow great, thank you so much.

Collapse
 
drew_e29d5b0152adc2 profile image
Drew Riley

These are all really great examples ( obviously #3 as an example of dependency injection rather than an actual way to generate a number) of clean code that is not slow code. But keep in mind the term clean code often is synonymous with slow code. Don't abstract just for the sake of it.

Collapse
 
ram_taylorabigadol_421ad profile image
Ram Taylor Abigadol

Great article!,
I love the focus on readability it is by far once of the most underrated issues today as javascript is so flexible you can do anything in n^2 ways.
I also love that you did give room for the question of readability vs performance, most article choose one or the other.
The best thing I can say for this article is that I think it should be a basis for code review processes.
so shkran.

Collapse
 
chris_sd_b9194652dbd4a1e profile image
Chris S-D

This is one of the best articles on clean coding I've seen for JavaScript in a long time. There's a couple areas that could probably be described in a bit more detail so that the intent isn't so easily misinterpreted, but overall this is really good.

I've been writing JavaScript since the year it came out. Over the years I've discovered things that work and things that don't. The things you have here are in the group of things that work (when applied properly).

You even included one that always seems to throw people off (the one under scalability where you are using an object literal in place of a switch or multiple if/else blocks). I love that one. Once people understand it, it's very elegant and, in my experience, frequently faster than the alternatives.

I think there are even more things that can really improve the JS experience, but so far, this hits most of the major things. Kudos.

Collapse
 
gopikrishna19 profile image
Gopikrishna Sathyamurthy • Edited

Everything looks great! I follow clean coding/coder principles as well. Great job 🤜!!

However...

No 3. Testability example? Um.. uh... No easy way to say this 😵‍💫: Nope. I was reading in a mobile, the code was cut off, and I thought you were sending in a seed for predictability, but sending in a function that replaces the only job of the system under test? Nope. Can you give a better example, please? Like, Math.random() takes a seed, send that as argument from test file.

I understand this is only a 3 line example, but IMHO, the example, to the untrained mind, can present a whole list opportunities to get it wrong.

Testing is not hard, but people can make it hard. A weak test is no better than no test. Any code is testable. You wrote the code, you have the scenario in mind, set that scenario up and see how your code behaves. Repeat for each scenario. Finally, automate on each commit.

What I would suggest for a testability example is what you have in the explanation for No 3: Break it down. Modularize for testability. Cleaner dependency injection for predictability, like stubbing Math.random through external means and run the system under test.

Better yet, TDD. If you can't test it, you don't have to write code for it. That's how you get inherently test-able-ed clean code.

Happy coding ✨!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

tnx.

Collapse
 
webjose profile image
José Pablo Ramírez Vargas

For configuration, my best TypeScript work so far (v3 in beta): wj-config

It excels in large and complex projects, but can also be used easily in simpler ones. The best part is that it works identically in NodeJS projects as well as browser projects, so it covers all of your JavaScript needs.

Collapse
 
skhmt profile image
Mike 🐈‍⬛

For #4, i'd argue a switch is even better than both other options. The first example with a ton of if statements is clunky and the exact reason the switch exists. The second example needlessly splits up code that makes it less readable and doesn't catch errors like the case not existing.

Collapse
 
piso_wifi_ad5a80a17a8d183 profile image
pisowifi • Edited

This sounds like an excellent resource for anyone looking to improve their coding skills in JavaScript! Writing clean, maintainable code is crucial for long-term project success. Some key principles that might be covered include:

Readable Code: Use clear variable names, consistent formatting, and proper indentation.
Modular Design: Break code into smaller, reusable functions or modules.
10.0.0.0.110.0.0.1
Avoiding Complexity: Write simple logic and avoid deeply nested structures.
Documentation: Add comments where necessary to clarify intent.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

thank you.

Collapse
 
itamartati profile image
Itamar Tati

Great Article bro, Enjoyed reading, hope to see more from you!

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Thank you, brother! I really appreciate your feedback. I'll try to bring even better content in the future, Insha'Allah! 😊

Some comments may only be visible to logged-in visitors. Sign in to view all comments.