# How JavaScript Engines Handle Variable Lookups: LHS vs RHS Explained

> Understand how JavaScript engines perform variable lookups using LHS vs RHS. Learn how identifiers are classified for efficient scope resolution.

- Repository: [Kyle Simpson/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
- Tags: deep-dive
- Published: 2026-02-24

---

**The JavaScript engine classifies every identifier as either a target (LHS) or source (RHS) during compilation, enabling direct lexical environment resolution without costly runtime scope-chain traversals.**

The distinction between **LHS** (left-hand side) and **RHS** (right-hand side) lookups forms the foundation of JavaScript's scope resolution mechanism. According to the *You Don't Know JS* repository by Kyle Simpson, the engine performs this classification during the compile-time phase to optimize runtime variable access. Understanding this process reveals why closures work efficiently and how the engine handles undefined variables.

## Compile-Time: Marking Targets and Sources

During the **compilation phase**, the JavaScript parser walks the abstract syntax tree (AST) and builds a **scope map** that records every identifier declaration and its intended usage. As detailed in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md) at line 199, the parser marks each occurrence of a name as either a **target** (an assignment destination) or a **source** (a value consumer).

This labeling attaches a conceptual "marble" to each identifier, representing its scope bucket and role. Declarations such as `var`, `let`, `const`, function names, and class names are recorded in their defining lexical scope. Because the engine knows upfront whether an identifier will receive values or provide them, it avoids expensive runtime analysis.

## Runtime Resolution: How the Engine Finds Variables

When execution begins, the engine leverages the compile-time scope map to resolve identifiers efficiently. Rather than walking through every scope in the chain for each access, the engine **directly resolves the identifier** to the appropriate lexical environment.

As illustrated in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md) (lines 18-26), lookups follow the lexical environment chain only until finding the first scope bucket containing the target name. The search stops at the nearest matching scope, yielding constant-time resolution for already-known variables.

### LHS (Target) Lookups

An identifier becomes a **target** when it receives an assignment. This includes explicit assignments and implicit bindings:

- Variable declarations with initialization: `let counter = 0`
- Reassignment operations: `nextStudent = getStudentName(73)`
- Loop headers: `for (let student of students)` where `student` is auto-assigned each iteration

According to [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md) (lines 97-103), the engine treats these occurrences as LHS references because they represent destinations for values.

### RHS (Source) Lookups

All non-assignment occurrences are **source** lookups, where the engine reads a value:

- Function arguments: `getStudentName(73)` requires an RHS lookup for the identifier `getStudentName`
- Property access bases: `console.log(nextStudent)` performs RHS lookups for both `console` and `nextStudent`
- Arithmetic operands: `counter++` reads the current value of `counter` via RHS before the increment assignment

## Why the Distinction Matters

The LHS/RHS classification enables two critical engine optimizations:

**Memory Management** – The engine can safely drop a variable's scope bucket entry once no longer needed, specifically after a target's function execution completes. As described in [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md), closures retain only the variables actually referenced by inner functions (sources), not every variable in the outer scope.

**Error Handling** – Failed lookups generate different behaviors based on context. A failed **source** lookup always throws a `ReferenceError`. A failed **target** lookup also throws in strict mode, as detailed in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md) (lines 241-247), preventing accidental global variable creation.

## Practical Examples

Consider the following pattern demonstrating both lookup types:

```javascript
// LHS: target receives assignment
let counter = 0;

// RHS: source provides value for the increment operation
// LHS: target receives the incremented value
counter++;

// RHS: source lookup for console and counter
console.log(counter);

// Mixed LHS/RHS in loop construct
// RHS: students is a source (read to iterate)
// LHS: student is a target (assigned each iteration)
for (let student of students) {
    // RHS: student is a source (read for property access)
    console.log(student.name);
}

```

When this code executes, the engine resolves `students` once as a source and creates fresh `student` bindings for each iteration as targets. No repeated scope-chain traversal occurs for `students` after the initial lookup, demonstrating the performance benefit of compile-time classification.

## Summary

- **LHS lookups** identify assignment targets (destinations), while **RHS lookups** identify value sources.
- The engine marks these roles during compilation in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md), attaching scope information to identifiers.
- Runtime resolution uses the lexical environment chain but stops at the first matching scope bucket, as shown in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md).
- Failed RHS lookups always throw `ReferenceError`; failed LHS lookups throw in strict mode.
- The distinction enables efficient memory management and precise closure behavior detailed in [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md) and [`scope-closures/ch7.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch7.md).

## Frequently Asked Questions

### What happens if the JavaScript engine cannot find an RHS variable?

The engine throws a `ReferenceError` immediately. Since RHS lookups represent value consumption, an unresolved source indicates the program is attempting to read a non-existent variable, which is always an error in both strict and non-strict mode.

### How does the engine handle LHS lookups for undeclared variables in non-strict mode?

In non-strict mode, a failed LHS lookup historically caused the engine to create a new global variable to serve as the assignment target. However, modern engines and strict mode throw a `ReferenceError` instead, preventing accidental pollution of the global namespace, as documented in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md).

### Why is the LHS/RHS distinction important for understanding closures?

The distinction determines which variables a closure retains. Because the engine knows which outer variables are sources (RHS) accessed by inner functions, it can optimize memory by keeping only those specific variables in the closure scope, rather than preserving the entire outer scope bucket.

### Does the engine actually walk the scope chain at runtime?

No, not in the way commonly conceptualized. While the engine must traverse the lexical environment chain to find the correct scope bucket, the compile-time scope map allows it to skip unnecessary scopes and stop at the first match. This optimized lookup process avoids the costly full-chain walks that would occur without prior LHS/RHS classification.