Writing Human Readable Code: To Name or Not To Name
Bola Adebesin

Bola Adebesin @mobolanleadebesin

About: While my approach to programming is both analytical and methodical, my passion for technology and discovery drives me to pursue creative solutions.

Location:
Baltimore, MD
Joined:
Dec 1, 2020

Writing Human Readable Code: To Name or Not To Name

Publish Date: Feb 23
17 17

I waffled back and forth about the title of this post, but in the end, I couldn't resist the Hamlet reference.

Today's topic is about writing clear, human-readable code. I care about this for a few reasons. When code is easier to read/understand, it's easier to:

  • Maintain
  • Refactor
  • Discuss (with clients, colleagues, your rubber duck etc).

There are so many instances where I've come across old code that I don't understand.

Star Wars Meme about difficult to read code

This frustrating experience has ignited my interest in learning how to craft "better" code.

The good news is, there are a bunch of blog posts, forums, books, and articles about how to write better code. A few tips I've seen:

  • Add comments to your code; Comments that explain "why" are better than comments that explain "what"
  • Write tests
  • Name your variables clearly
  • The single responsibility principle: every function should do one job really well
  • Avoid complicated conditionals
  • Keep files to 250 lines of code or less

I coud go on, but there is one piece of advice that really struck me and I want to discuss it. The advice was: "Use traditional function declarations and opt for named functions over anonymous ones."

This advice caught my attention because my code is littered with anonymous functions. I can't count how many unnamed functions I've passed into .map, .forEach, and .reduce methods.

I don't think I'm alone in this. I've read time and time again that arrow functions and anonymous functions help to make code more concise and that this brevity makes for smaller files, which is part of writing better code, right? Well, it turns out that concise and anonymous does not necessarily mean better or more readable.

The more I think about this, the more it makes sense. "Well-named variables" is one of the bullet items for achieving clearer code. Variables with clear names help us to understand the data contained within them. This should also be true for functions which not only contain data, but often manipulate that data in some way too.

Let's look at a small example pulled from the Frontendmasters course, "Deep JavaScript Foundations V3" by Kyle Simpson, author of the "You don't know JS" series.

I am going to share two different implementations I wrote for a function called printRecords. One uses named functions and the other does not.

The function, printRecords should:

  • Accept an array of numbers (these numbers represent student Ids)
  • Retrieve each student record using the student Id
  • Sort these records alphabetically by the students' names
  • Print each record to the console in the following format: Name (student Id): Paid (or Not Paid)
var currentEnrollment = [410, 105, 664, 375] 
var studentRecords = [
    { id: 313, name: "Frank", paid: true, },
    { id: 410, name: "Suzy", paid: true, },
    { id: 709, name: "Brian", paid: false, },
    { id: 105, name: "Henry", paid: false, },
    { id: 502, name: "Mary", paid: true, },
    { id: 664, name: "Bob", paid: false, },
    { id: 250, name: "Peter", paid: true, },
    { id: 375, name: "Sarah", paid: true, },
    { id: 867, name: "Greg", paid: false, },
];

// Named Functions Version: 
function printRecords(recordIds){
  function findStudentRecords(recordId){
     function hasStudentId(record){
       return recordId == record.id; 
     }
     return studentRecords.find(hasStudentId);
  }
  function alphabetizeRecordsByName(a,b){
    return a.name > b.name ? 1: -1; 
  }
  function logRecord(record){
    console.log(
      `${record.name} (${record.id}): ${record.paid ? 'Paid': 'Not Paid'}`
     );
  }
  recordIds.map(findStudentRecords)
  .sort(alphabetizeRecordsByName)
  .forEach(logRecord); 
}

//Anonymous Functions Version: 

function printRecords(recordIds){
    recordIds
        .map((recordId) => 
            studentRecords.find((studentRecord) => studentRecord.id == recordId)
        )
        .sort((a,b) => a.name > b.name ? 1: -1)
        .forEach((student) => {
            console.log(
                `${student.name} (${student.id}): ${student.paid ? 'Paid': 'Not Paid'}`
            )
        })
}


printRecords(currentEnrollment); 


// Console: 
/*
 Bob (664): Not Paid
 Henry (105): Not Paid
 Sarah (375): Paid
 Suzy (410): Paid
*/

Enter fullscreen mode Exit fullscreen mode

Looking at the two implementaions side-by-side in my editor, I can see that the named version of printRecords spans lines 1-23, while the anonymous version spans lines 1-12. Some of this is a result of the automated code formatter I use, but right away it's clear that the anonymous version is more concise.

Still, when I look at the named version, I understand what each function in the .map, .sort, and .forEach method is doing. I don't have to read the code line by line. It's all right there in the names: find the student records, sort them alphabetically by name, print each record. By contrast, the anonymous version requires me to look at the code in the body of each function to understand what is happening.

In this case, I would rather take the extra 11 lines of code for a better experience modifying this code down the road.

In his lecture, Kyle Simpson posits that there are at least three reasons to choose named functions over anonymous ones:

  1. Reliable self-reference (e.g. If a function has a name, it can do things like call itself for a recursive solution)

  2. More debuggable Stack Trace (if the function has a name, the stack trace will provide it)

  3. More self documenting code

What do you think? Would you give up anonymous functions if it meant more human readable code? Do you use the number of lines of code as an indicator of whether a piece of code needs to be rewritten? How do ensure your code is readable 2 months down the road and beyond?

Update March 13, 2025:

I received some really great feedback on this post that I wanted to acknowledge because it encouraged me to think more (a.k.a I went down a bunch of rabbit holes) about naming conventions and not just clarity of code, but also the clarity that inherently exists (or doesn't) in human language itself.

Thank you:
@moopet
@devpacoluna
@pengeszikra
@lionelrowe
@decaf_dev
@ddfridley

The comments you left are great jump off points for me and others to continue learning and growing should they stumble across this post in the future!

** Image by Tumisu from Pixabay

Comments 17 total

  • David
    DavidFeb 24, 2025

    As a software engineer with decades of experience I found the anonymous function version easier to read. I also point out that English has a problems with being imprecise and having different meanings for different people in different contexts. I do appreciate good variable names and comments about why, but somehow shorter is quicker and easier to digest.

    • Bola Adebesin
      Bola AdebesinFeb 24, 2025

      @ddfridley Thanks for your comment! You're right, English can be imprecise, and I hadn't considered the confusion that might arise from that. Maybe it's be worth it to let the code, speak for itself.

  • Paco Luna
    Paco LunaFeb 24, 2025

    As a matter of fact, you can use named functions to create a very useful library to your project. Just use anonymous functions that you certainly know that are very simple or one in a million in your project.

    • Bola Adebesin
      Bola AdebesinFeb 24, 2025

      @devpacoluna Thanks for your comment! I hadn't thought of using named functions for building a library, is that something you've done?

      • Paco Luna
        Paco LunaFeb 25, 2025

        yep, I work with react. So i create a folder lib/ and each file has some functions with the same purpose example date.ts, car.ts, etc...

  • Ben Sinclair
    Ben SinclairMar 4, 2025

    I like this and agree mostly, but I think the examples you've given add to the confusion rather than making it clearer:

    Reading the following, I'd be confused expecting getStudentRecordById to take an ID as a parameter and to return a record object, when in fact it takes a record object and returns a boolean. That's suitable for use in the find method, but it doesn't make sense in itself:

      function findStudentRecords(recordId){
         function getStudentRecordById(record){
           return recordId == record.id; 
         }
         return studentRecords.find(getStudentRecordbyId);
      }
    
    Enter fullscreen mode Exit fullscreen mode

    If I was reading this one, printEachRecord would imply to me that it was a function which took some kind of iterable, rather than a single record:

      function printEachRecord(record){
        console.log(
          `${record.name} (${record.id}): ${record.paid ? 'Paid': 'Not Paid'}`
         );
      }
    
    Enter fullscreen mode Exit fullscreen mode
    • Bola Adebesin
      Bola AdebesinMar 12, 2025

      @moopet thanks for your comment. You raise a good point and one that someone in another comment brought up.

      Named functions can only provide additional clarity if they are named well. And there is definitely room for improvement for how I named these functions.

  • decaf Dev
    decaf DevMar 5, 2025

    I personally like to take the approach of bringing the helper functions out of the important business logic while using something like ramda to make that magic possible:

    import { curry } from "ramda";
    
    /**
     * @param {{id: number, name: string, paid: boolean}[]} records 
     * @param {number[]} ids 
     */
    export function printRecords(records, ids) {
      const byRecordIsInListOfIds = curry(recordIsInListOfIds)(ids);
      records.filter(byRecordIsInListOfIds)
        .sort(compareRecordNames)
        .forEach(printRecord);
    }
    
    /** 
     * @param {number[]} ids
     * @param {{id: number}} record
     */
    function recordIsInListOfIds(ids, record) {
      return ids.includes(record.id);
    }
    
    /**
     * @param {{name: string}} a 
     * @param {{name: string}} b 
     */
    function compareRecordNames(a, b) {
      return a.name > b.name ? 1 : -1;
    }
    
    /**
     * @param {{id: number, name: string, paid: boolean}} record 
     */
    function printRecord(record) {
      console.log(
        `${record.name} (${record.id}): ${record.paid ? 'Paid' : 'Not Paid'}`,
      );
    }
    
    Enter fullscreen mode Exit fullscreen mode

    Here the curry method will allow us to do some fancy functional programming and not have to resort to something like recordIsInListOfIds.bind(null, ids).

  • lionel-rowe
    lionel-roweMar 5, 2025

    Given the simplicity of the functions in your example, I think the anonymous/inline version is significantly more readable. That's partly due to the increased conciseness, which cuts out unnecessary noise, but mostly to do with the reduced indirection compared to the named version. Unnecessary indirection is really bad for readability, because it forces you to jump around instead of reading linearly.

    If each step contained more logic, named would probably be better (indirection can be a good thing when dealing with significant chunks of abstraction), but in that case most functions should be defined at the top level rather than nested. That way they can be tested individually.

    The exception is where the function depends on variables in the outer scope, which are sometimes better defined nested within that scope. Other options would be refactoring to take all their dependencies as explicit arguments, or using a class with methods and internal state.

    • Bola Adebesin
      Bola AdebesinMar 12, 2025

      @lionelrowe thanks for your comment. I agree the logic is simple enough that it might not be the best example.

      My thought was that readers would see the potential in this style of code for larger, more complex code bases, even if the examples were simple.

      Although, as others have pointed out, this approach requires the names of the functions to be clear, which I could have done a better job on too.

      I appreciate the feedback and perspective!

  • Peter Vivo
    Peter VivoMar 5, 2025
    // named arrow version
    
    const findStudent = (recordId) => studentRecords
      .find((studentRecord) => studentRecord.id == recordId);
    
    const nameSorting = ({name: aName},{name: bName}) => aName > bName 
        ? 1
        : -1;
    
    const consoleRender = (student) => console.log(
        `${student.name} (${student.id}): ${student.paid ? 'Paid': 'Not Paid'}`
    );
    
    const printRecords = (recordIds) => recordIds
            .map(findStudent)
            .sort(nameSorting)
            .forEach(consoleRender)
    ;
    
    Enter fullscreen mode Exit fullscreen mode
    • Bola Adebesin
      Bola AdebesinMar 13, 2025

      Thanks for sharing this. I like that your version combines arrow functions and semantic naming

  • Kirill Artamonov
    Kirill ArtamonovMar 29, 2025

    There are only two hard things in Computer Science: cache invalidation and naming things.

    -- Phil Karlton

    Named functions are great when they describe common concepts. E.g., I'd have quite some confidence in how sum looks, even if it were declared in a different file:

    const numbers = [1, 2, 3]
    const sum = (acc, n) => acc + n
    numbers.reduce(sum)
    
    Enter fullscreen mode Exit fullscreen mode

    But for functions with different possible implementations (especially the logRecord, where log structure shapes the feature's "look and feel"), I'd start with anonymous functions in order to understand the entire logic as I read the code line by line—without the need to scan the file (or even jump between files) for the declaration of a specific function. If the context of a specific piece of code gets too big, I'd look for ways to extract coherent parts. I find it important to let the code and your understanding of it evolve for a while, rather than zealously extract every anonymous function.

Add comment