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;
Good:
const radius = diameter / Math.PI;
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;
}
Good:
function calculateArea(radius) {
return Math.PI * radius * radius;
}
3. Testability
Clean code is inherently testable. Break down complex operations into smaller, verifiable units.
Bad:
function getRandomNumber() {
return Math.random();
}
Good:
function getRandomNumber(randomGenerator = Math.random) {
return randomGenerator();
}
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
}
}
Good:
const userHandlers = {
admin: handleAdminData,
user: handleUserData,
guest: handleGuestData
};
function handleUserData(data) {
return userHandlers[data.type](data);
}
Common Pitfalls and Solutions:
1. The Naming Dilemma
Names should reveal intent and context.
Bad:
function calc(a, b) {
return a * b + TAX;
}
Good:
function calculatePriceWithTax(basePrice, taxRate) {
const TAX_MULTIPLIER = 1;
return basePrice * taxRate + TAX_MULTIPLIER;
}
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...
});
});
});
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);
}
}
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';
Good:
// config.js
export const config = {
api: {
key: process.env.API_KEY,
endpoint: process.env.API_ENDPOINT
}
};
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;
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);
}
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)
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."
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.
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
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).
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.
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.
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.
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.
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.
What about mocks?
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.
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.
Your comment is completely incorrect. This function is an excellent example of dependency injection.
Advantages of this approach:
Example:
This is an efficient design pattern that promotes:
The default parameter Math.random ensures normal operation, while allowing custom generators when needed - a standard dependency injection technique in JavaScript.
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.
I agree .. all this hidden "magic" is so fubared after you come back on a project after a while ...
Hello ! Don't hesitate to put colors on your
codeblock
like this example for have to have a better understanding of your code 😎wow great, thank you so much.
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.
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.
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.
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 ✨!
tnx.
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.
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.
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.1 – 10.0.0.1
Avoiding Complexity: Write simple logic and avoid deeply nested structures.
Documentation: Add comments where necessary to clarify intent.
thank you.
Great Article bro, Enjoyed reading, hope to see more from you!
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.