• Home
  • Posts
  • Books
  • About
  • KR

Why Do We Feel Some Code Is Easier to Read?

Developer Intuition, Code Readability, and Neuroscience


Why Do We Feel Some Code Is Easier to Read?

I previously wrote a post called “What Is Good Code? On the Illusion of Readability” where I discussed how subjective and context-dependent the formula “good code = readable code” really is.

The conclusion that people judge readability differently is fair enough. But if you trace the mechanics behind it, where that sense of “this is easy to read” actually comes from, you start to find some hints.

So that’s what this post is about. How humans understand code, and what makes certain forms of information feel easier to process.

What Happens in the Brain When We Read Code

A 2020 fMRI study at MIT looked at which brain regions light up when programmers read code.

It turns out code reading primarily activates the Multiple Demand Network, the region for logical reasoning and complex cognitive tasks, not the language network. The language regions weren’t completely uninvolved, but the Multiple Demand Network response was dominant.

Put simply, when our brains read code, they rely more on logical reasoning than language processing.

Five brain regions related to working memory, attention, and language processing showed pronounced activation during code comprehension, while Default Mode Network activity (the regions active when the brain is resting) decreased. Reading code is a fairly expensive operation for the brain.

So far, this might seem unsurprising. But the more interesting part is the difference between experts and novices. Skilled developers showed lower overall brain activation levels when reading code. Not because they were trying less, but because they were processing more efficiently. Novice developers showed widespread brain activation, meaning they were consciously processing nearly everything one piece at a time.

Novices tended to read code like natural language text, top to bottom. Experts followed the program’s execution flow. Same code, fundamentally different processing.

Working Memory: 4 Slots

To understand why some code reads easily while other code refuses to organize itself in your head, you need to know about working memory.

In 1956, cognitive psychologist George A. Miller published “The Magical Number Seven, Plus or Minus Two”, showing that human short-term memory capacity is roughly 7±2 items. Later research revised this downward. Cowan (2001) found the number of chunks you can hold in working memory at once is closer to 3–4. It varies by task and expertise, but either way, it’s smaller than you’d think.

Think about what we load into our heads when reading code. The current value of a variable, the direction of control flow, the order of function calls, the state of the current scope… all of these occupy working memory slots. When those slots fill up, the brain struggles to take in new information. That’s the moment you get that “this code feels complicated” sensation.

To put it in programming terms, working memory is like a fixed-size stack.

Real working memory is a far more complex system than a stack, but the analogy works well enough: there’s a capacity limit, and exceeding it causes things to break down. The stack size is roughly 4, and exceeding it causes a stack overflow. In practice, the items occupying each slot aren’t uniform in size, and interference between items also occurs, so the exact number matters less than the fact that capacity is limited. The moment that capacity hits its limit is when you feel “this code is hard to read.”

Let’s compare code that rapidly exhausts working memory with code that doesn’t.

// Code that rapidly exhausts all 4 working memory slots
function processOrder(order: Order) {
  if (order.status === 'pending' && order.items.length > 0) {
    const discount = order.customer.tier === 'premium'
      ? order.items.reduce((sum, item) => sum + item.price, 0) * 0.1
      : order.coupon?.discount ?? 0;

    const tax = (order.total - discount) * (order.shipping.domestic ? 0.1 : 0);
    const finalPrice = order.total - discount + tax + order.shipping.cost;

    return { ...order, finalPrice, status: 'processed' };
  }
  return order;
}

This function isn’t wrong, but it demands too many things be held in the reader’s working memory simultaneously.

The condition on order.status, the check on order.items, the discount branching by customer.tier, the null check on the coupon, the domestic/international tax branching, the final price calculation… trying to fit all of this context into 3–4 slots is what makes the brain scream.

If we can break this context into appropriately sized units, we can save working memory slots needed to understand each operation.

// Version that reduces working memory load
function calculateDiscount(order: Order): number {
  if (order.customer.tier === 'premium') {
    const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
    return subtotal * 0.1;
  }
  return order.coupon?.discount ?? 0;
}

function calculateTax(amount: number, shipping: ShippingInfo): number {
  return shipping.domestic ? amount * 0.1 : 0;
}

function processOrder(order: Order) {
  if (order.status !== 'pending' || order.items.length === 0) {
    return order;
  }

  const discount = calculateDiscount(order);
  const tax = calculateTax(order.total - discount, order.shipping);
  const finalPrice = order.total - discount + tax + order.shipping.cost;

  return { ...order, finalPrice, status: 'processed' };
}

In this version, the amount you need to hold in your head at each step is noticeably reduced. The behavior is identical, but by breaking information into consistent units, how much context occupies each working memory slot becomes manageable.

When reading calculateDiscount, you only need to focus on discount logic. When reading processOrder, you can follow the overall flow without knowing each calculation’s internals. Each function fits within working memory’s capacity, which is why it feels easier to understand. This is also why abstraction at the right granularity matters in design. Of course, the reverse is true too. Splitting things too finely means you spend working memory jumping between functions and tracking context. The goal isn’t splitting for its own sake, but reducing how much you need to hold in your head at once.

Chunking: The Brain’s Data Compression Algorithm

But if working memory only has about 4 slots, how do we comprehend hundreds of lines of code?

The answer is chunking: grouping multiple small units of information into a single meaningful cluster. Research from the University of Zurich found that chunking reduces working memory load by swapping individual elements for compressed representations pulled from long-term memory. The freed-up capacity then goes to processing new incoming information.

Think about phone numbers. Trying to remember 01012345678 as eleven individual digits would far exceed working memory capacity. But split it as 010-1234-5678, and you only need to remember three chunks. Better yet, if 010 is already familiar as “Korean mobile prefix,” it’s automatically processed as a single chunk, meaning you really only need to memorize two new chunks.

The same thing happens with code. Look at what happens when a skilled developer reads this:

const activeUsers = users.filter(u => u.isActive).map(u => u.name);

A novice developer has to load each individual element into working memory: the users variable, how .filter works, arrow function syntax, the u.isActive property access, how .map works, another arrow function…

For a skilled developer, users.filter(...).map(...) is recognized as a single chunk: “filter array then transform.” Having seen this pattern hundreds or thousands of times, they pull a stored chunk from long-term memory and use just one working memory slot. The remaining slots can be allocated to higher-level thinking like “why is this being filtered?” or “where does the result get used?”

This is the same principle that allows chess masters to glance at a board and grasp the entire situation. Chess masters don’t memorize individual piece positions one by one. They recognize familiar formation patterns as single chunks. That’s why they can quickly recall meaningfully arranged boards but perform no better than novices with randomly placed pieces.

Code works the same way. Idiomatic code is easy to read not because it’s “correct,” but because it matches chunks already stored in the developer’s long-term memory.

What makes a pattern idiomatic is shaped by external factors: the language’s design intent, standard library conventions, the community’s repeated choices. It’s not simply “familiar because it’s used a lot.” Convergence driven by multiple forces created that familiarity. This is also why project-specific unique patterns or overly creative code are hard to read. When existing chunks don’t match, the brain has to decompose everything into individual elements, and working memory saturates almost instantly.

System 1 and System 2: Intuition and Analysis

This is where Daniel Kahneman’s dual process theory comes in. Kahneman divided human thinking into two systems:


  • System 1: Fast, automatic, intuitive thinking. Based on pattern recognition.
  • System 2: Slow, conscious, analytical thinking. Based on logical reasoning.

Most of our everyday judgments are handled by System 1. Driving a car, sensing your partner’s mood from a single word over the phone. When encountering new information, System 1 doesn’t create new patterns; it matches against patterns already stored.

When System 1 gets stuck, that’s when System 2 gets called in. The moment you consciously think “what is this?” and start analyzing, that’s System 2 engaging. Apply this framework to code reading, and a lot starts to make sense.

Readable code is code that’s mostly processed by System 1’s pattern recognition, with minimal System 2 involvement.

When skilled developers read code, familiar patterns are handled automatically by System 1. for loops, if-else branches, map/filter/reduce chaining, try-catch blocks. These have been seen thousands of times, so they’re processed without conscious effort. System 2 stays in a comfortable low-power mode, just approving the information that System 1 surfaces.

But when an unexpected pattern appears, things change.

// Code that System 1 can handle
const canPurchase = user.age >= 18 && user.isVerified;

// Code that invokes System 2
const canPurchase = !(user.age < 18 || !user.isVerified) && (user.age !== undefined);

Both express the same intent. But the moment you see the second version, System 1 sends an “I don’t know” signal, and System 2 begins its expensive analysis. You have to unwind the double negation, run De Morgan’s law in your head, and figure out why that final undefined check is necessary. This transition itself is a cognitive cost.

Kahneman called this cognitive strain. The more System 2 has to step in, the more energy the brain burns and the more fatigued you feel. The “this code is hard to read” sensation is the cognitive cost of System 1 failing to match a pattern and handing things off to System 2.

From this angle, feedback like “this is hard to understand” in code reviews isn’t just taste. It’s closer to a signal that system-switching costs are actually occurring. The magnitude varies by individual experience and chunk composition, but the cost itself is real.

Gestalt Principles: How Visual Structure Affects Code Comprehension

Readable code depends on visual structure too, not just logical structure. This is where Gestalt psychology comes in.

Gestalt psychology studies how the brain prioritizes overall patterns and structures over individual elements. A few of its principles connect directly to code readability.

Law of Proximity

Elements that are close together are perceived as a group. Let’s look at two versions of the same code to see why this grouping matters.

// Code without proximity principle applied
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');
const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);
// Code with proximity principle applied
const name = user.firstName + ' ' + user.lastName;
const email = user.email.toLowerCase();
const isValid = email.includes('@') && email.includes('.');

const role = determineRole(user.permissions);
const dashboard = getDashboard(role);
const notifications = getNotifications(user.id, role);

The second version just adds a single blank line, yet the brain automatically recognizes two groups: “user info processing” and “permission-based data retrieval.”

Gestalt research shows that proximity is an even stronger grouping cue than similarity of color or shape. UI design uses this directly: related information goes close together, unrelated information gets spaced apart.

Similarly, blank lines and indentation matter in code not just for aesthetics, but because the brain’s perceptual system uses them to parse structure.

Law of Similarity

Like proximity, elements that look alike are also perceived as belonging to the same group. This explains why consistent naming matters in code.

// Naming that violates similarity
const userData = fetchUser(id);
const get_orders = retrieveOrderList(userId);
const pmtHistory = loadPayments(uid);
// Naming that follows similarity
const user = fetchUser(id);
const orders = fetchOrders(id);
const payments = fetchPayments(id);

In the first version, all three are the same kind of operation (data fetching), but the naming conventions are all over the place. userData, get_orders, pmtHistory each have different forms, and fetchUser, retrieveOrderList, loadPayments lack any consistency. The brain can’t recognize these as the same group and processes each line as a separate item, consuming working memory.

In the second version, the pattern is clear. The consistent fetch + resourceName structure causes all three lines to be recognized as a single chunk: “data fetching in the same pattern.”

Law of Continuity

The law of continuity states that we perceive elements as a single continuous entity when they follow a natural flow of direction. In code, this relates to the linearity of execution flow.

Our brains find top-to-bottom, left-to-right flow natural. Deep nesting, complex callbacks, and scattered goto statements are hard to read because they violate the law of continuity.

This is also why early return patterns feel easier to read than nested if statements. Once edge cases are filtered out, the remaining code flows in a single direction from top to bottom.

// Code that breaks continuity
function getPrice(user: User, product: Product): number {
  if (user.isActive) {
    if (product.inStock) {
      if (user.tier === 'premium') {
        return product.price * 0.8;
      } else {
        if (product.onSale) {
          return product.salePrice;
        } else {
          return product.price;
        }
      }
    } else {
      throw new Error('Out of stock');
    }
  } else {
    throw new Error('Inactive user');
  }
}
// Code that maintains continuity
function getPrice(user: User, product: Product): number {
  if (!user.isActive) throw new Error('Inactive user');
  if (!product.inStock) throw new Error('Out of stock');

  if (user.tier === 'premium') return product.price * 0.8;
  if (product.onSale) return product.salePrice;

  return product.price;
}

The first version forces your eyes to zigzag right and left as indentation deepens. The brain has to maintain “which condition am I currently inside” in working memory at each nesting level.

The second version filters out exceptions first, then flows naturally from top to bottom. Because it aligns with the law of continuity, the cost for the brain to parse the structure drops significantly.

Cognitive Load Theory: Three Types of Load

John Sweller’s Cognitive Load Theory gives a more systematic framework for everything above.

It breaks cognitive load during learning or problem-solving into three types:


  1. Intrinsic load: The inherent complexity of the task itself. Algorithm difficulty, domain complexity, and so on.
  2. Extraneous load: Unnecessary complexity from presentation that’s irrelevant to the task. Inconsistent naming, unnecessary indirection, confusing code structure.
  3. Germane load: The beneficial load spent forming and learning new schemas.

The takeaway for writing readable code: minimize extraneous load.

Intrinsic load can’t be reduced. It’s inherent to the problem. Code implementing a distributed consensus algorithm will be complex no matter how well it’s written. The problem is when extraneous load from “how it’s expressed” piles unnecessarily on top of intrinsic load.

From this perspective, consistent naming, appropriate function decomposition, clear type declarations, meaningful blank lines: these all reduce extraneous cognitive load. They keep the brain’s limited resources from being wasted on presentation issues so you can focus on the actual problem.

A 2023 systematic literature review found something interesting here. Among studies comparing source code metrics to measured cognitive load, few traditional metrics showed consistent correlation with what developers actually experienced. Results varied depending on task conditions and measurement methods. What we “measure” as complexity and what a developer’s brain “feels” as complex can be quite different.

Mechanical metrics capture surface-level complexity, but the cognitive load you actually experience depends on how familiar the patterns are, how efficiently your brain chunks them, how clearly the visual structure maps to the logic. These are subjective factors that no metric fully captures.

Experience Physically Changes the Brain

I mentioned earlier that expert and novice brain activation patterns differ. This isn’t just “more experience = better.” A 2024 study in Scientific Reports measured brainwaves from 62 Python programmers while showing them code with intentionally inserted syntax errors and semantic errors.

Skilled programmers showed distinct brainwave patterns for syntax violations versus semantic violations, the same way our brains produce different responses to grammatical errors versus meaning errors in natural language. Programming experience forms specialized neural circuits for processing specific languages and patterns.

Coding experience physically changes the brain. Through neuroplasticity, patterns you encounter repeatedly get stored as schemas in long-term memory, and these schemas become the foundation for chunking. System 1 can automatically process code because these schemas have accumulated over time.

Because schemas form based on the patterns an individual has been exposed to, people who repeatedly encounter the same patterns in the same codebase end up sharing similar schemas. Flip this around, and maintaining a consistent code style within a team isn’t about taste. It’s the process of forming shared chunks in team members’ brains. Coding conventions matter not for uniformity’s sake, but for collective cognitive efficiency.

Predictive Coding: The Brain Doesn’t Read Code, It Predicts

Predictive coding theory offers another angle. The brain doesn’t passively receive information. It constantly predicts what will come next and only does extra processing when the actual input differs from its prediction.

The same applies when reading code. Our brains constantly predict what the next line will be.

async function fetchUserProfile(userId: string) {
  try {
    const response = await api.get(`/users/${userId}`);
    // Your brain is already predicting what comes next

A skilled developer’s brain reading this code is already predicting “it’ll probably parse the response data and return it.” And when return response.data; actually appears, the prediction was correct, so virtually no additional cognitive cost is incurred.

But what if something completely unexpected shows up?

async function fetchUserProfile(userId: string) {
  try {
    const response = await api.get(`/users/${userId}`);
    globalEventBus.emit('user-fetched', response.data); // Wait, what?
    localStorage.setItem('lastUser', JSON.stringify(response.data)); // Huh?
    analytics.track('profile_view', { userId }); // Why is logging here?
    return response.data;

What the brain predicted from the name fetchUserProfile was “a function that fetches a user profile and returns it.” But then event bus emission, localStorage writes, and analytics tracking appear, all outside the prediction. Each time, the brain generates a prediction error signal and invokes System 2 to analyze why this code is here.

The reason the second version feels hard to read is that the function is named fetchUserProfile, but internally it does things that have nothing to do with that name. The more closely a name’s set expectation matches the actual behavior, the fewer prediction errors occur and the less cognitive cost is incurred.

This applies not just within functions but to interface design as well. Let me use component interfaces familiar to frontend developers like myself as an example.

// Predictable interface
<TextInput
  value={name}
  onChange={setName}
  placeholder="Enter your name"
/>
// Hard-to-predict interface
<UserNameInput
  user={name}
  setUser={setUser}
  blank="Enter your name"
/>

The first component follows value, onChange, placeholder, an interface shared by virtually every input component in the React ecosystem. A developer’s System 1, having encountered this pattern countless times, processes it as “it’s an input component” and moves on.

The second uses user, setUser, blank, an interface either tightly coupled to business logic or with unclear meaning. Until you figure out which fields of the user object it touches internally and when blank is actually used, you can’t use it with confidence. You have to look at the internal implementation every time, and each time, prediction errors occur.

To write readable code, you need to maximize predictability at every level: function names, variable names, file structure, directory organization, API design.

So What Is “Good Code,” Really?

Putting it all together, “readable code” is code that:


  1. Doesn’t exceed working memory (context stays within 3–4 items)
  2. Matches existing chunks (familiar patterns that activate long-term memory schemas)
  3. Can be processed by System 1 (understandable through pattern recognition, without invoking System 2)
  4. Has visual structure that matches its logical structure (Gestalt principles let the brain parse structure automatically)
  5. Is predictable (doesn’t violate expectations set by names and structure)
  6. Has low extraneous cognitive load (strips away presentation complexity, leaving only the problem’s inherent complexity)

Notice something about this list? None of these mean “correct code.”

Readability and correctness are separate axes. Code can be perfectly correct but impossible to read, and code can be easy to read but wrong. That said, readable code does make bugs easier to find. With lower extraneous load, the brain can focus its resources on finding logical errors.

There’s one caveat, though. Kahneman also warned about System 1’s biases. System 1 is fast, but that speed comes at the cost of bias. A classic example is familiarity bias. It’s true that you feel patterns familiar to you as “readable code,” but whether that’s objectively optimal is a separate question. A developer fluent in functional programming might find for loops “hard to read,” and vice versa. In those cases, the feeling of “hard to read” may not reflect the code’s objective quality but the bias of chunks stored in your own System 1.

When giving “this is hard to understand” feedback in code reviews, it’s worth asking yourself once: is this genuinely high-cognitive-load code, or is it simply a pattern not registered in my System 1? If the former, refactoring is needed. If the latter, it might actually be an opportunity to expand your chunk library.

Wrapping Up

“Readable code” isn’t just a matter of feeling or taste. It’s a natural outcome of how our cognitive systems work: limited working memory, chunking, dual process thinking, Gestalt perception, predictive coding.

The reason I said “readability is subjective” in my previous post comes down to this. Chunking depends on individual experience. The patterns registered in System 1 differ from person to person.

When two people look at the same code and one finds it easy while the other finds it hard, that’s not just a difference in preference. The schemas accumulated in their brains are different. Readability is inevitably subjective because it’s baked into our cognitive structure.

ProgrammingNeuroscienceReadabilityCognitive PsychologyDX

관련 포스팅 보러가기

Dec 23, 2024

What Is Good Code? On the Illusion of Readability

Essay