Rubber duck in dishwasher
Cover image for ron

Ron Northcutt Verified userVerified user

Head of DevRel

Appsmith

Coding Clean: Write Better Code Now

You may not know this about me, but I am an expert in cleaning the kitchen. No - seriously. When we have our large Thanksgiving dinner with multiple families and 25-30 people, I am the first one to get started on kitchen duty. Last year, we set a record for cleaning up (25 minutes!!!). Of course, it all comes down to practice. 

My wife went to a culinary school and is an amazing chef, so we eat really well... but her skills mean that we often have more things made from scratch or close, and the kitchen takes a beating. The rule in our family is "whoever cooks does not clean," so I typically get the cleaning job (not complaining - it is worth it). So, I put in my headphones, pop on an audiobook, and get to cleaning. A remarkable sense of accomplishment comes from starting with a big mess and ending with a clean and organized space.

As developers, it is all too easy to get a "dirty kitchen" when cooking up some awesomeness. This always leads to problems down the road - bugs, errors, poor performance, lost time debugging, trying to understand the code, etc. The solution is to work on the skills and habits of writing "clean code." Clean code lays the foundation for an efficient, collaborative, and adaptable codebase. It prevents common problems before they even start and sets up your future self for success.

Just like cleaning and organizing the kitchen, writing clean and well-organized code is a skill that takes time and effort. The good news is that with just a handful of basic patterns, you can instantly write better code with only a little bit more effort. The better news is that once you start doing this, it will make you faster and more successful in the future.

Note: the code examples here are Javascript, but most of these guidelines work for every language. Adapt to your language and your project's coding style, but write good code.

What is "clean code"?

Clean code is characterized by its readability, understandability, and modifiability. It lacks needless intricacies and repetitiveness and adheres to a series of conventions and best practices, facilitating collaborative efforts on shared projects. Clean code is easy to read, easy to understand, and easy to modify. Additionally, it is less prone to bugs and errors because the act of "cleaning" removes many of the vectors where problems can creep in.

To be fair, this doesn't mean your code will be error-free, performant, scalable, etc... you still need to worry about those things. But it does mean it will be easier to work on, and you will avoid the most common issues. 

Why clean code?

We will talk about specific tactics shortly, but let's clarify the benefits first. Why on earth should you even care about "clean code" (whatever that means), and why should you spend the time and energy to create and maintain it? 

  • Readability: Clean code ensures swift comprehension, making it faster to develop and debug.
  • Maintainability: Clean code supports the evolution and growth of applications by easing future modifications.
  • Collaboration: It simplifies collaboration, enabling team members to simultaneously work on different code sections.
  • Minimized Bugs: Ensuring your code is simple and intentional will avoid common errors and make it easier to fix problems.
  • Efficiency: Optimized, clean code often results in faster execution and minimized resource utilization.

Most developers are nodding their heads at this point. Sure, sure, sure... make it readable, limit bugs, easier to maintain... ok. But the whole point is how do we ensure our code is "clean." There are many ways to do this, but there are a few key guidelines that almost everyone can agree on. The thing to keep in mind is that these are *guidelines*... not rules. There are no rules in programming - if it compiles, it will run. These guidelines are mostly for humans - to help us create, understand, fix, extend, and improve our code. 

How do I create clean code?

  1. Descriptive Identifiers

    Choose clear, descriptive names for variables, functions, and classes. This helps you improve code readability, especially when calling or using those things. You can use camel case, snake case, or any other approach you like... but try to be consistent.

    // NO: Less clear variable name
    i = [5, 8, 2];
    // YES: Descriptive variable name
    selectedItems = [5, 8, 2];
    
    // NO: Gross code to work with
    function upc(i) {
      let a = calc(i);
      let b = tax(a);
      let c = a + b;
      return [a, b, c];
    }
    
    // YES: Lovely code to work with
    function updateCart(items) {
      let subtotal = calculateSubtotal(items);
      let tax = calculateTax(subtotal);
      let total = subtotal + tax;
      return [subtotal, tax, total];
    }     

    In this second example, you can easily understand what the function is trying to do and how it is getting it done. You can read and understand the code even without any comments (see #3). This makes it easier to update or extend later and makes it easier to find potential areas for refactoring (see #8).

  2. Short and Single-Purpose Functions

    Ideally, your functions should be small and concentrate on a solitary task, adhering to the Single Responsibility Principle (SRP). This will help you limit complexity and reduce bugs right off the bat. It also makes it much easier to QA and test your code. If there is a problem, you can test each function individually and more quickly find the problem. 

    // NO: Monolithic function that is hard to test and extend.
    function processAndDisplayScore(input) {
      // Validation
      if (typeof input !== "number" || input < 0 || input > 100) {
        console.error("Invalid input");
        return;
      }
    
      // Calculate score
      let score = "F";
      const gradeRanges = new Map([
        [90, "A"],
        [80, "B"],
        [70, "C"],
        [60, "D"],
      ]);
    
      for (let [minScore, grade] of gradeRanges) {
        score = input >= minScore ? grade : score;
      }
      // Display message
      console.log(`Your score is: ${score}`);
    }
    processAndDisplayScore(78); // Your score is: C

    In the case above, you can see that you have a single large function doing many things. It is not too complicated now, but that can change over time. If we can write this as smaller functions, we introduce the ability to reuse these pieces in different ways or at least make it easier to debug and understand later.

    Protip: if your function is over 30 lines, has multiple ifs, or has multiple returns, it might be best to be broken up. Ideally, most of your functions have fewer than twenty lines and a single return statement.

    // YES: More modular code is easier to read, test, use, and extend.
    const gradeRanges = new Map([
      [90, "A"],
      [80, "B"],
      [70, "C"],
      [60, "D"],
    ]);
    
    function validateInput(score) {
      return typeof score === "number" && score >= 0 && score <= 100;
    }
    
    function calculateGrade(score) {
      let letterGrade = "F";
      for (let [minScore, grade] of gradeRanges) {
        letterGrade = score >= minScore ? grade : letterGrade;
      }
      return letterGrade;
    }
    
    function processGrade(score) {
      let message = "";
      if (!validateInput(score)) {
        message = "Invalid score input";
      } else {
        const grade = calculateGrade(score);
        message = `Your grade is: ${grade}`; // Fixed string formatting
      }
      return message;
    }
    
    function displayGrade(score) {
      let message = processGrade(score);
      console.log(message);
    }
    
    displayGrade(78); // Your grade is: C

    In this case, we started with a single function, which is 22 lines of code, and we broke it into four different functions, about 34 lines of code. Some might think that adding 12 lines of code is not so good, but 11 are either blank or function wrappers. So it is really only a single extra line (which could be further reduced).

    The advantage here is that now we have much simpler functions that can be debugged more easily. Also, we have set ourselves up for future success. For example, if we decide to send a message differently, we only need to change one line in a single function. Or, if we want to change our grading system to include +/-, we can change the constant. The odds of breaking anything else are very small.

  3. Good Use of Comments

    Comments should be used generously, offering clarity when the code isn’t self-explanatory. If you use good descriptive identifiers, then you will need fewer comments. However, adding more useful comments to your code is always a good idea. This can be another way to check your work by ensuring that your logic makes sense when written out. It is also super helpful for ensuring that the next person who has to work on your code will have an easier time.

    Protip: always comment your code as if the next developer to work on it is an axe-wielding maniac who knows where you live...

    // NO: Redundant comment
    z = z + 1; // Increment z
    
    // YES: Update the totalCount by increasing z
    totalCount = z + 1;

    Using block comments for your functions and methods is also a good idea. Your code may never be crawled for documentation or have rigorous testing that requires type hinting, but it is a good practice to have. You want your code to be professional and solid, regardless of who sees it or how it is used. This habit will serve you well in your life and career.

    Protip: Don't want to take the time to write out comments? This is a great use for ChatGPT - ask it to write comments and drop in a code snippet. 

    /**
     * Calculates the letter grade based on a numeric score.
     * 
     * @param {number} score - The numeric score (0-100) for which the grade needs to be determined.
     * @returns {string} The letter grade (A, B, C, D, or F) corresponding to the input score.
     */
    function calculateGrade(score) {
        let letterGrade = "F";
        for (let [minScore, grade] of gradeRanges) {
            letterGrade = (score >= minScore) ? grade : letterGrade;
        }
        return letterGrade;
    }

    Keep in mind that comments don't have to be dry and boring. In fact, sometimes, adding a bit of humor in code comments can be very helpful. 

  4. Uniform Formatting

    Ensure consistent coding style and indentation. Adhering to community-accepted coding standards improves the visual clarity of your code. This can be done with a good IDE that understands code formatting rules, some code sniffer tools, or even AI. You can use any coding style you want (let's not get into tabs vs spaces), but the key is consistency in your codebase.

    // NO: Non-uniform formatting
    if (condition) {
    doTask();
    }elseif (otherCondition){ doOtherTask();
    }
    
    
    // YES: Uniform formatting
    if (condition) {
      doTask();
    }
    elseif (otherCondition) (
      doOtherTask();
    }

    The code in both examples is the same, but the first one is a mess. It is hard to parse and understand, and even if it works, it will be tough to spot errors. Are we missing a closing semicolon or brackets? I dunno... it's too convoluted.

    In the second example, a quick scan reveals exactly what's going on, and it is easy to see that everything seems to be in order. We can all agree that it would be nicer to work on whatever codebase the second example came from versus the first one. In fact, I would be worried about the first codebase and probably avoid using or working on it.

  5. Abide by the DRY Principle     

    DRY is an acronym for "Don't Repeat Yourself". It means you shouldn't write the same code multiple times - you should try to create common functions that can be used for common tasks. This has several benefits.

    First, it will likely reduce the size of your code, making it smaller and faster (to a certain degree). Next, it will allow you to centralize your work - you can fix a bug or make an improvement in one place instead of many. Also, it makes it easier to read and understand the code.

    Protip: You can be more DRY when you see repeating code patterns. This is another great reason to ensure consistent formatting (#4).

    // NO: Repeated code
    function computePriceForShirt(quantity, shirtPrice) {
        return quantity * price;
    }
    function computePriceForShoes(quantity, shoePrice) {
        return quantity * price;
    }
    
    // YES: Unified function
    function computeItemPrice(quantity, price) {
        return quantity * price;
    }

    This is a simple example, but you can see how much less code we have. This also means we can make changes much easier. For example, if we wanted to extend this to lookup the price (with a possible discount based on quantity), we can do it much easier in a single place rather than in multiple places.

    // Calculate the price
    function computeItemPrice(quantity, sku) {
        let price = priceLookup(sku);
        let discount = getDiscount(quantity) || 1;
        return quantity * price * discount;
    }
  6. Effective Use of Whitespace     

    I'm unsure if every developer goes through a "minimalist" phase, but it seems common. The minimalist goal is to write the most streamlined code in as few lines and characters as possible. Perhaps it is the result of coding games.

    However, it is largely a waste of time and energy. Most systems will aggregate, minify, compress, or otherwise remove excess white space as part of the build. Ultimately, your code will be compiled, and what the computer is actually running is not what you wrote directly. So, while a coding game may be a fun challenge, it's a pretty horrible way to create and maintain your code.

    Ultimately, your code is for humans - not the computer. If it were for the computer, we would just write in assembly or some other lower-level language. All popular coding languages today are higher level - they are intended to be easy to write and read by humans... and will be compiled to machine code later. Keep your code optimized for you and your team's use. The computer will take care of itself.

    // NO: Packed code
    const multiply=(x,y)=>{return x*y;}
    
    // YES: Spaced code
    const multiply = (x, y) => {
        return x * y;
    }                                
  7. Graceful Error Management     

    Handle errors elegantly through try-catch blocks or equivalent mechanisms, providing constructive debug information without compromising the system's stability. This will help uncover potential edge cases where things don't quite work.

    You should also be mindful of creating descriptive and useful error messages. This will be incredibly helpful when you run into a problem - a well-crafted error can give you an immediate idea of the problem and how to fix it - before you even look at the code! 

    // Improved error handling
    try {
        result = operation(x, y);
    }  catch (error) {
        console.error("Error encountered: ", error.message);
    }                                
  8. Periodic Refactoring

    Continually refine your code. As the understanding of the project deepens, modify the code to enhance its clarity and efficiency. In this example, we have some code that applies a discount based on a role type. Most likely, this happens over time with new roles being added. However, this starts to get a bit dirty and harder to maintain:

    function getDiscount(customerType) {
        if (customerType === 'student') {
            return 20; // 20% discount
        } else if (customerType === 'veteran') {
            return 10; // 10% discount
        } else if (customerType === 'senior') {
            return 15; // 15% discount
        } else if (customerType === 'employee') {
            return 25; // 25% discount
        } else {
            return 0; // no discount
        }
    }
    console.log(getDiscount('student')); // 20                     

    At this point, we can improve the quality and readability of this code by using a Map. This is much easier to maintain and harder to break and introduce a regression. This type of improvement won't make a major difference today, but it may help tomorrow when you need to refactor the code further to provide a lookup for discounts or add new roles.

    const discountMapping = new Map([
        ['student', 20],
        ['veteran', 10],
        ['senior', 15],
        ['employee', 25]
    ]);
    
    function getDiscount(customerType) {
        return discountMapping.get(customerType) || 0;
    }
    console.log(getDiscount('student')); // 20                

    Plus - this is just nicer to look at and read. It also makes it much easier for another developer to update or extend it in the future without creating a bigger mess.

    Warning: It can be easy to fall into the "perfection" trap with refactoring or creating code. Do your best to create quality code, follow best practices, and solve the problem. Trying to make your code "perfect" is a waste of your time.

    Protip: To avoid spending too much time cleaning up your existing code, just mark certain areas for refactoring with a nice @TODO comment, and then take regular time to go through and improve it later. That way, you won't forget and can make time to do it properly.

  9. Version Control

    Utilize systems like Git to monitor code changes, enabling collaborative work, clean historical tracking, and ease of reverting when required. This is even more helpful when you are working on a team and need to merge changes across different workstreams collaboratively. 

    Version control is also a great way to reduce stress and protect yourself in case of deletions or errors. You can always "go back in time" and recover or review old code. You can also contain different workstreams on different branches to keep your code clean without losing anything.

    As a bonus on projects with multiple developers over time, you can also use git blame it to figure out exactly who wrote or changed every line of code. That's really helpful when you are looking to find a person who understands the code or if you are looking to give constructive feedback to another developer. 

  10. Keep Dependencies Updated

    Regularly update the dependencies your project relies on. This ensures security, compatibility, and access to the latest features and performance improvements. Typically, this is a fairly safe thing to do, but you should plan to do some basic testing to ensure that any changes made don't have other unexpected effects.

    It is also easier to deal with any dependency changes or adjustments in smaller pieces. If you allow too much time to go by, you could end up in a situation where multiple dependencies (and their security/bug fixes) are out of date, but multiple issues need to be adjusted. This can turn a relatively simple issue into a major problem when you need to juggle multiple refactors or change libraries. We've all seen projects that are effectively "dead" because they are so out of date that they need to be recreated entirely. 

Clean code transcends a set of rules; it embodies a mindset. It’s the pursuit of crafting software that remains comprehensible, maintainable, and extensible. A deliberate effort to study diverse coding practices, especially from open-source projects, yields rich insights into refined coding methodologies. It's a continuous journey where, with consistent practice, crafting clean code becomes instinctive, paving the way for optimized software development.

Over time and with more experience, these best practices become second nature. While paying attention to writing clean code may take a bit more effort at first, eventually, it becomes your default process. When you get to that level, you will find that your code and how you think about code evolves to a higher level. The investment will be repaid many times over.