How JavaScript Evolved from ES1 to Modern ES6

Trace how JavaScript grew from a simple scripting language in ES1 to the powerful, modern language of ES6. See what changed at each version and why it matters.

7 min read

JavaScript evolved from ES1 to ES6 over 18 years, changing from a simple scripting language into the modern language you write today. The version you use now, with const, arrow functions, template literals, and async/await, is the product of that decades-long evolution. Here is how JavaScript grew into what it is now.

What Each Version Contributed

ECMAScript version evolution

There was no ES4. Work stalled after 2003 and TC39 formally abandoned the effort in 2008 once browser vendors could not agree on its scope. The language went from ES3 in 1999 directly to ES5 in 2009, a decade with no major updates.

ES3 (1999): The Language Most People Learned

For a decade, ES3 was JavaScript. Before this version, the language had no error handling and no regular expressions. ES3 added the features that made JavaScript usable beyond toy scripts, starting with pattern matching on strings:

javascriptjavascript
const email = "user@example.com";
const isValid = /^[^\s@]+@[^\s@]+$/.test(email);
console.log(isValid);

The regular expression checks that the string has text before and after an @ symbol with no spaces, a simple email shape check. Running it against a valid address prints:

texttext
true

ES3 also introduced try/catch, so a single bad input no longer had to crash the whole script. Code that might fail goes inside the try block, and any resulting error is handled here:

javascriptjavascript
try {
  JSON.parse("not valid json");
} catch (error) {
  console.log("Failed to parse:", error.message);
}

Code inside the try block runs normally, and if it throws an error, the catch block receives that error instead of stopping the program. The switch statement rounded out ES3 by giving developers a cleaner way to handle multiple fixed cases than a long chain of if and else statements.

ES5 (2009): The Quiet Revolution

After a decade of stagnation, ES5 modernized JavaScript without breaking compatibility. Its additions were subtle but transformative.

Strict mode caught common mistakes that would otherwise fail silently:

javascriptjavascript
"use strict";
x = 10;

The "use strict" line at the top switches the file into strict parsing rules. Assigning to a variable that was never declared now throws instead of silently creating one:

texttext
ReferenceError: x is not defined

Before ES5, that same typo would have silently created a global variable and kept running, hiding a real bug. Strict mode turns that silent failure into a visible one.

ES5 also added functional array methods that replaced manual loops for common transformations:

javascriptjavascript
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled);

Before .map(), doubling every number in an array meant writing a for loop with a counter and a manually pushed result array. Calling it with a small function does the same work in one line:

texttext
[2, 4, 6, 8, 10]

.map(), .filter(), and .reduce() changed how developers thought about transforming data, turning what used to take five lines into a single method call.

JSON support became native, so parsing and building JSON no longer required a third-party library:

javascriptjavascript
const obj = { name: "Alice", age: 30 };
const json = JSON.stringify(obj);
console.log(json);

JSON.stringify() converts a JavaScript object into a JSON string that can be sent over the network or saved to a file. Running it on a simple object produces:

texttext
{"name":"Alice","age":30}

JSON.parse() does the reverse, turning a JSON string back into an object. ES5 was not flashy, but it made JavaScript feel less like a toy language. For a deeper look at how the standard body works, see the history of ECMAScript and JavaScript standardization.

ES6 / ES2015: The Language Transformed

ES6 was the update developers had been waiting 16 years for. Every feature replaced a real pain point.

let and const: Sane Variable Declarations

javascriptjavascript
// Before ES6: var has function scope and redeclares silently
var count = 1;
var count = 2; // No error. Confusing.
if (true) {
  var count = 3; // Same variable! Overwrites the outer one.
}
console.log(count); // 3, probably not what you expected

The var keyword ignores block boundaries, so the if block above quietly overwrites the outer variable. The let keyword fixes this by respecting block scope, and const works the same way for values that never get reassigned:

javascriptjavascript
let score = 1;
if (true) {
  let score = 3; // A different variable, scoped to this block
  console.log(score); // 3
}
console.log(score); // 1, the outer variable is unchanged

Each let declaration is contained to the block it is written in, so the inner and outer score variables never collide.

Arrow Functions: Shorter and Lexical this

javascriptjavascript
// Before ES6: verbose function keyword
const numbers = [1, 2, 3];
const doubled = numbers.map(function(n) {
  return n * 2;
});
 
// After ES6: concise arrow function
const doubled2 = numbers.map(n => n * 2);

Arrow functions also fixed the notorious this binding problem in callbacks, since they inherit that value from the surrounding code instead of redefining it on every call.

Template Literals: Readable String Building

javascriptjavascript
const name = "Alice";
const items = 3;
 
// Before ES6: string concatenation
const message1 = "Hello, " + name + ". You have " + items + " items.";
 
// After ES6: template literals
const message2 = `Hello, ${name}. You have ${items} items.`;

Both lines produce the same string, but the template literal is easier to read because the variables sit inside the text instead of being stitched together with plus signs.

Destructuring: Clean Data Extraction

javascriptjavascript
// Before ES6: manual extraction
const user = { name: "Bob", age: 25, city: "London" };
const userName = user.name;
const userAge = user.age;
 
// After ES6: destructuring
const { name, age } = user;
console.log(name, age); // "Bob", 25

Destructuring pulls named properties directly off an object into their own variables in one line, instead of assigning each property one at a time.

Modules: Organized Code at Last

Before ES6, splitting code across files meant relying on global scripts or community patterns like CommonJS. ES6 added a native way to share code between files:

javascriptjavascript
// math.js
export const add = (a, b) => a + b;
 
// app.js
import { add } from "./math.js";
console.log(add(2, 3)); // 5

The export keyword makes a value available to other files, and import brings it in by name. Every function or variable stays private to its file unless explicitly exported.

Promises: Escape from Callback Hell

Before ES6, chaining several asynchronous steps meant nesting callbacks inside callbacks, a pattern developers called callback hell:

javascriptjavascript
fetchUser(id, function(user) {
  fetchOrders(user.id, function(orders) {
    console.log(orders);
  });
});

Each level of nesting adds another layer of indentation, and the code becomes harder to follow as more steps are added. Promises replaced that nesting with a flat, readable chain:

javascriptjavascript
fetchUser(id)
  .then(user => fetchOrders(user.id))
  .then(orders => console.log(orders))
  .catch(error => console.error(error));

Each .then() call runs after the previous step finishes, and a single .catch() at the end handles errors from any step in the chain.

Classes: Familiar OOP Syntax

Before ES6, creating an object type meant writing a constructor function and attaching methods to its prototype by hand:

javascriptjavascript
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  return "Hello, " + this.name;
};

This pattern works, but the connection between the constructor and its methods is easy to miss at a glance. ES6 wrapped that same pattern in familiar class syntax:

javascriptjavascript
class Person {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hello, ${this.name}`;
  }
}

The class syntax is sugar over prototypes. JavaScript still uses prototypal inheritance underneath, but the syntax is what most developers expect from an object-oriented language.

The Pattern: Every Feature Solved a Real Problem

The evolution from ES1 to ES6 follows a clear pattern. Each feature was not added for decoration. It was added because developers had been working around a real limitation for years:

Pain PointES6 Solution
var scope confusionlet and const
Verbose function syntaxArrow functions
String concatenation messTemplate literals
Manual property extractionDestructuring
No standard module systemimport and export
Callback pyramidsPromises
Awkward OOP patternsClasses
No native data structuresMap, Set, WeakMap, WeakSet
Repeated arguments-object manipulationRest parameters and spread

This pattern continues with every yearly ECMAScript release since ES6. For the broader story of JavaScript's journey, see the complete history of JavaScript explained.

Rune AI

Rune AI

Key Insights

  • ES1 (1997) was the first standard, defining basic syntax and data types.
  • ES3 (1999) added regex, try/catch, and switch -- the baseline for a decade.
  • ES5 (2009) added strict mode, JSON, and functional array methods.
  • ES6 (2015) was the largest update, adding let/const, arrows, classes, modules, and promises.
  • Every ES6 feature replaced a real pain point that developers had worked around for years.
RunePowered by Rune AI

Frequently Asked Questions

What is the biggest difference between ES5 and ES6?

ES6 added block-scoped variables (let/const), arrow functions, classes, modules, promises, template literals, and destructuring. Before ES6, JavaScript code looked dated and required workarounds for basic patterns. After ES6, it looked like a modern language.

Can I use ES6 features in all browsers?

Yes. All modern browsers fully support ES6 (ES2015) features. Internet Explorer 11 does not support most ES6 features, but IE11 is no longer supported by Microsoft and has negligible market share.

Why did ES4 never ship?

ES4 was too ambitious. It proposed classes, static typing, and major structural changes that browser vendors could not agree on. After years of stalled progress, TC39 formally abandoned the effort in 2008, and meaningful JavaScript updates were delayed until ES5 in 2009.

Conclusion

JavaScript's evolution from ES1 to ES6 is the story of a language growing from a simple browser scripting tool into a powerful general-purpose programming language. ES3 stabilized the language for a decade. ES5 quietly modernized it. ES6 transformed it with features that developers now use every day. Understanding this evolution helps you appreciate why modern JavaScript looks the way it does and how each feature solved a real problem.