# JavaScript Compilation vs Interpretation: How JS Engines Really Work

> Discover how JavaScript engines compile code into bytecode for faster execution unlike line-by-line interpretation. Understand the technical difference and its impact on performance and error detection.

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

---

**JavaScript engines compile source code into bytecode before execution, parsing the entire program to establish lexical scope and catch early errors, rather than interpreting line-by-line.**

Modern JavaScript execution involves distinct compilation and runtime phases that fundamentally differ from pure interpretation. According to the getify/You-Dont-Know-JS repository, understanding this pipeline is essential for predicting scope behavior, debugging effectively, and writing performant code.

## The Compilation Phase

As documented in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md) and [`get-started/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/get-started/ch1.md), JavaScript engines perform a complete compilation pass before any code runs. This phase transforms human-readable source into an executable format through four critical steps.

### Lexing

The engine first breaks source text into tokens. This initial scan catches illegal characters and malformed tokens immediately, preventing invalid code from reaching the execution stage.

### Parsing into an AST

Tokens assemble into an **Abstract Syntax Tree (AST)** representing program structure, statements, and expressions. During this step, the engine determines **lexical scope** for every identifier. As noted in [`scope-closures/ch5.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch5.md), declarations using `var`, `let`, and `const` are "handled entirely by the compiler" during this phase, establishing scope boundaries before runtime.

### Byte-code Generation

The AST transforms into engine-specific **bytecode** (intermediate representation). This compact format serves as the input for the **JIT (Just-In-Time) compiler**, which later optimizes hot execution paths while the program runs.

### Early-Error Checking

Syntax errors, duplicate parameters, and illegal `use strict` constructs surface during compilation. As explained in [`get-started/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/get-started/ch1.md), these "early errors" provide **static feedback** before any side effects occur, ensuring the program never partially executes with invalid syntax.

## The Execution Phase

After compilation, the engine interprets the generated bytecode—not the original source text. This distinction enables **Just-In-Time (JIT) compilation**, where frequently executed code paths are re-compiled into optimized machine code during runtime ([`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md)).

The `eval()` function demonstrates this separation clearly. When invoked, `eval` receives a string, compiles it on-the-fly into bytecode, and then executes it within the current scope. This runtime compilation capability proves that interpretation happens only after a compilation step, even for dynamically generated code.

## Why JavaScript Compilation vs Interpretation Matters

Understanding whether JavaScript is compiled or interpreted impacts code quality, debugging, and performance in four key ways.

**Error Detection Timing**  
Compilation catches syntax and scope errors before execution begins. Unlike pure interpreters that might crash mid-program, JavaScript validates the entire script upfront, preventing partially-executed states.

**Performance Optimizations**  
Knowing the complete program structure allows engines to apply **inline caching**, **dead-code elimination**, and **hidden-class optimizations**. These transformations require global code knowledge impossible to obtain in line-by-line interpretation.

**Scope Predictability**  
Lexical scope is immutable after compilation. Variable hoisting, block scoping, and function declarations resolve once during the compilation phase, eliminating runtime scope surprises and ensuring consistent identifier resolution.

**Tooling and Static Analysis**  
Linters, type-checkers, and IDEs mirror the engine's compilation phase to provide accurate diagnostics. This static analysis capability depends on the parser's ability to build a complete AST before execution.

## Practical Examples

The following examples from the You-Dont-Know-JS source illustrate the compilation-execution pipeline in practice.

### Normal Compilation Flow

```javascript
function add(a, b) {
  return a + b;   // The function body is compiled once into byte-code.
}
console.log(add(2, 3));

```

The engine parses the entire file, hoists the `add` identifier, and compiles the function body into bytecode before execution begins. At runtime, the engine simply invokes the pre-compiled bytecode, yielding `5`.

### Runtime Compilation with `eval`

```javascript
let x = 10;
eval('let x = 20; console.log(x);'); // Compiles the string *on the fly*.
console.log(x);                      // Still 10 – the outer scope unchanged.

```

The string passed to `eval` compiles during execution, creating a temporary scope that shadows the outer `x`. Because this compilation occurs at runtime, it forces the engine to re-analyze code dynamically, invalidating previous optimizations and degrading performance.

### Early Error Detection

```javascript
"use strict";
function duplicate(a, a) {} // SyntaxError: Duplicate parameter name not allowed.

```

The compiler detects the duplicate parameter before any code runs, throwing a `SyntaxError` during the compilation phase. No side effects occur, and the program never enters the execution phase.

## Summary

- JavaScript engines compile source code into bytecode **before** execution, parsing the entire program into an AST to establish lexical scope.
- **Early errors** caught during compilation prevent partial program execution and provide static debugging feedback.
- The **JIT compiler** optimizes hot bytecode paths during runtime, combining compilation benefits with dynamic execution speed.
- `eval()` demonstrates runtime compilation by parsing and executing strings dynamically, though at a performance cost.
- Understanding the **JavaScript compilation vs interpretation** distinction explains hoisting, scope behavior, and why syntax errors surface before any code runs.

## Frequently Asked Questions

### Is JavaScript interpreted or compiled?

JavaScript is most accurately characterized as a **compiled language**. Modern engines perform a complete parsing and compilation phase to generate bytecode before execution begins, as documented in [`get-started/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/get-started/ch1.md). While the engine interprets the resulting bytecode, it never interprets raw source code line-by-line.

### When does JavaScript compilation happen?

Compilation occurs **immediately before execution** when the script loads. The engine lexes, parses, and generates bytecode for the entire program scope, determining variable declarations and catching syntax errors. This happens separately from the runtime phase where the compiled bytecode actually executes.

### Why does JavaScript compile instead of interpret?

Compilation enables **early error detection** and **performance optimizations** impossible with pure interpretation. By analyzing the complete AST beforehand, engines can optimize hot code paths through JIT compilation, establish immutable lexical scopes, and catch syntax errors before any side effects occur.

### How does `eval` work if JavaScript is compiled?

The `eval` function receives a string and invokes the compiler **at runtime**, generating fresh bytecode from the string content before executing it. This proves that compilation and execution remain distinct phases—even dynamically generated code must compile before it can run, though this runtime compilation bypasses initial optimizations and degrades performance.