Javascript JS Foundation & Core Concepts ES5, ES6, and the ESNext

If you’ve been following along with this JavaScript series, you’ve already covered a solid amount of ground. We talked about what JavaScript actually is, how it runs inside the browser, your very first JavaScript program, the syntax fundamentals, and how to write useful comments. Now it’s time to zoom out a bit and understand the language itself as a living, evolving thing.

Here’s a situation most beginners hit fairly quickly: you’re reading two different tutorials online. One uses var, the other uses let. One writes functions like function greet() {}, the other writes them as const greet = () => {}. You start wondering if you’ve accidentally opened something outdated, or if one approach is wrong.

Neither is wrong. JavaScript has changed massively over the years, and what you’re seeing are different eras of the language. This article explains what that means, why it happened, and how to navigate it confidently.


01ECMAScript and JavaScript: Two Names, One Language

People use “JavaScript” and “ECMAScript” like they’re the same thing, and for most practical purposes, they are. But there’s a distinction worth knowing.

ECMAScript is the official specification. It’s a document that defines how the language should work: what syntax is valid, how types behave, what built-in methods exist, and so on. JavaScript is the most popular implementation of that specification, the one that runs in every browser and in Node.js.

The name “ECMAScript” came about like this: Brendan Eich created JavaScript in 1995 at Netscape in about ten days. It was immediately popular. Microsoft then released their own version called JScript. Two browsers, two slightly incompatible dialects, one messy web.

To fix this, Netscape submitted the language to ECMA International, a standards organization, in 1996. They published the first specification in 1997 as ECMAScript 1. The name “ECMAScript” was the result of a compromise since “JavaScript” was technically a trademark owned by Sun Microsystems (later Oracle). So the spec became ECMAScript, the implementation stayed JavaScript, and both names have been used side by side ever since.

When you see things like “ES6”, “ES2015”, or “ES2022,” those are all referring to specific versions of the ECMAScript specification.


02The ECMAScript Version Timeline

Before we get into what actually changed and why it matters, here’s a visual overview of the major ECMAScript releases. Click any version to see what it brought.

Interactive
ECMAScript Version History
ES1
1997
ES1
ES3
1999
ES3
ES5
2009
ES5
ES6
2015
ES2015
ES7
2016
ES2016
ES8
2017
ES2017
ES9
2018
ES2018
ES10
2019
ES2019
ES11
2020
ES2020
ES12
2021
ES2021
ES13
2022
ES2022
ES14
2023
ES2023
ES15
2024
ES2024
ECMASCRIPT 1 · 1997
The First Official Standard
The spec that started it all. ES1 took the existing Netscape JavaScript and formalized it into an official standard so all browser vendors had something concrete to implement. Very basic compared to what we have today.
Basic syntax
Operators
Statements
Built-in objects

You’ll notice there’s a big gap: ES3 came out in 1999, and the next major version, ES5, didn’t land until 2009. That’s a full decade. What happened?

Work started on ES4, which was extremely ambitious. It proposed things like classes, optional static typing, a module system, and more. But the browser vendors couldn’t agree. After years of debate, ES4 was abandoned entirely. The “Harmony” project started fresh, took the most sensible ideas, and eventually that became ES5 and later ES6.


03ES5: The Version That Stabilized Everything

ES5 landed in 2009, and while it wasn’t flashy, it was exactly what the language needed at the time.

The big things it added:

  • Strict mode. You could now write "use strict"; at the top of a file or function to opt into a stricter version of JavaScript that catches silent errors and prevents some bad patterns. If you’ve ever seen that at the top of a file, that’s where it came from.

  • New array methods. forEach, map, filter, reduce, some, every. These became the foundation of functional-style JavaScript that developers still use daily.

  • JSON support. JSON.parse() and JSON.stringify() were built into the language. Before ES5, you needed a third-party library just to work with JSON safely.

  • Better object methods. Object.keys(), Object.create(), Object.defineProperty(), and getters/setters for object properties.

Here’s a quick taste of ES5 array methods in action:

// ES5 array methods
var numbers = [1, 2, 3, 4, 5];

// forEach: loop over each item
numbers.forEach(function(num) {
  console.log(num);
});

// map: transform each item and return a new array
var doubled = numbers.map(function(num) {
  return num * 2;
});
console.log(doubled); // [2, 4, 6, 8, 10]

// filter: return only items that pass a test
var evens = numbers.filter(function(num) {
  return num % 2 === 0;
});
console.log(evens); // [2, 4]

// reduce: combine all items into one value
var total = numbers.reduce(function(sum, num) {
  return sum + num;
}, 0);
console.log(total); // 15

Solid, useful stuff. But notice all those function keywords. By today’s standards it feels a bit verbose. That’s what ES6 fixed.


04ES6 / ES2015: The Release That Split JavaScript Into “Before” and “After”

If there’s one version of JavaScript you need to understand deeply, it’s ES6. Released in 2015, it was the biggest update the language had ever seen, and it modernized JavaScript completely. Almost everything you’ll read in modern tutorials, frameworks, and codebases uses ES6+ syntax.

Let’s go through the most important features one by one.

let and const: Finally, Better Variable Declarations

Before ES6, the only way to declare a variable was with var. The problem with var is that it has function scope, not block scope, which causes some genuinely surprising behaviors. (We’ll cover this in detail in the next batch when we get into variables specifically.)

ES6 added let and const. Both are block-scoped, which is what most developers expected variables to behave like all along.

// var leaks out of blocks
if (true) {
  var name = "Daniyal";
}
console.log(name); // "Daniyal" — var ignores the block

// let stays inside the block
if (true) {
  let city = "Lahore";
}
// console.log(city); // ReferenceError — city doesn't exist here

// const is for values that shouldn't be reassigned
const PI = 3.14159;
// PI = 3; // TypeError — you can't reassign a const

Use const by default. Reach for let when you know the value will change. Avoid var in modern code unless you have a specific reason.

Arrow Functions: Shorter and Smarter

Arrow functions are a more concise way to write functions. They also handle the this keyword differently from regular functions, which matters a lot once you get into objects and classes.

// ES5 function
var add = function(a, b) {
  return a + b;
};

// ES6 arrow function: same thing, less ceremony
const add = (a, b) => a + b;

// If there's only one parameter, you can skip the parentheses
const double = n => n * 2;

// If the body has multiple lines, use curly braces and return
const greet = (name) => {
  const message = "Hello, " + name;
  return message;
};

console.log(add(3, 4));    // 7
console.log(double(5));    // 10
console.log(greet("Ali")); // "Hello, Ali"

Template Literals: String Formatting, Done Right

Template literals use backticks instead of quotes and let you embed expressions directly into strings with ${}. No more string concatenation nightmares.

const name = "Sara";
const score = 98;

// ES5 way — messy
var message = "Good job, " + name + "! You scored " + score + " points.";

// ES6 template literal — clean and readable
const message = `Good job, ${name}! You scored ${score} points.`;

// You can put any expression inside ${}
const result = `2 + 2 equals ${2 + 2}`;

// Multi-line strings are also easy now
const html = `
  <div>
    <h1>Hello, ${name}</h1>
    <p>Your score: ${score}</p>
  </div>
`;

console.log(message); // "Good job, Sara! You scored 98 points."

Destructuring: Pulling Values Out Cleanly

Destructuring is a way to unpack values from arrays or objects into separate variables in one clean line.

// Array destructuring
const colors = ["red", "green", "blue"];

// ES5 way
var first = colors[0];
var second = colors[1];

// ES6 destructuring
const [first, second, third] = colors;
console.log(first);  // "red"
console.log(second); // "green"

// Object destructuring
const user = { name: "Hassan", age: 25, city: "Karachi" };

// ES5 way
var userName = user.name;
var userAge = user.age;

// ES6 destructuring
const { name, age, city } = user;
console.log(name); // "Hassan"
console.log(age);  // 25

Default Parameters: No More Manual Fallbacks

Before ES6, handling missing function arguments required awkward || tricks. Default parameters make this declarative and clear.

// ES5 way — clunky
function greet(name) {
  name = name || "stranger";
  return "Hello, " + name;
}

// ES6 default parameters — clear and direct
function greet(name = "stranger") {
  return `Hello, ${name}`;
}

console.log(greet("Usman"));  // "Hello, Usman"
console.log(greet());         // "Hello, stranger"

Spread and Rest: Three Dots That Do Two Different Things

The ... syntax serves two purposes depending on where you use it: rest collects multiple values into an array, and spread expands an array or object into individual values.

// REST: collects remaining arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15

// SPREAD: expands an array into individual values
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

// SPREAD with objects: copy and extend
const defaults = { theme: "dark", language: "en" };
const userSettings = { ...defaults, language: "ur", fontSize: 16 };
console.log(userSettings);
// { theme: "dark", language: "ur", fontSize: 16 }

Promises: Handling Asynchronous Code Properly

JavaScript often has to do things that take time, like fetching data from an API. Before promises, developers handled this with nested callbacks, which became notoriously messy (people called it “callback hell”).

A Promise is an object that represents a value that will be available sometime in the future. It can be pending, resolved (success), or rejected (failure).

// Creating a promise
const fetchUser = new Promise((resolve, reject) => {
  // Simulating an async operation with setTimeout
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve({ name: "Zara", role: "developer" });
    } else {
      reject("Failed to fetch user");
    }
  }, 1000);
});

// Using the promise
fetchUser
  .then(user => {
    console.log("Got user:", user.name); // "Got user: Zara"
  })
  .catch(error => {
    console.log("Error:", error);
  });

console.log("This runs immediately while the promise is pending...");

Classes: Cleaner Object-Oriented Code

JavaScript already had object-oriented programming through prototypes, but the syntax was awkward. ES6 classes are just a cleaner way to write the same thing. Under the hood, it’s still the prototype system, but the code reads much more naturally.

class Animal {
  constructor(name, sound) {
    this.name = name;
    this.sound = sound;
  }

  speak() {
    return `${this.name} says ${this.sound}!`;
  }
}

// Inheritance with extends
class Dog extends Animal {
  constructor(name) {
    super(name, "Woof");
  }

  fetch(item) {
    return `${this.name} fetches the ${item}!`;
  }
}

const myDog = new Dog("Bruno");
console.log(myDog.speak());       // "Bruno says Woof!"
console.log(myDog.fetch("ball")); // "Bruno fetches the ball!"

Modules: import and export

Before ES6, JavaScript had no native module system. If you wanted to split your code across multiple files, you relied on the order of script tags or third-party tools. ES6 modules let you export values from one file and import them in another.

// ---- math.js ----
// Named exports: export specific things by name
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

export const PI = 3.14159;

// ---- main.js ----
// Named imports: import exactly what you need
import { add, multiply, PI } from "./math.js";

console.log(add(3, 4));       // 7
console.log(multiply(2, PI)); // 6.28318

Now let’s compare ES5 and ES6 side by side for some real feel of how much more readable modern JavaScript is.

ES5 vs ES6 Side by Side
Select a feature to compare the old and new syntax




ES5 (Old Way)

ES6 (Modern Way)


05After ES6: The New Yearly Release Schedule

One of the important things ES6 changed wasn’t just features. It changed how JavaScript evolves. Before ES6, releases happened in unpredictable multi-year cycles. The ES4 disaster was partly a result of trying to do too much at once.

After 2015, TC39 (the committee in charge of JavaScript) switched to a yearly release schedule. Instead of waiting years for a massive release, new features would ship every June in a smaller batch. That’s why you see ES2016, ES2017, ES2018… all the way to today.

This is also why the old number naming (ES6, ES7, ES8…) gave way to year-based names (ES2015, ES2016, ES2017…). It reflects the annual cadence.


06How New Features Get Added: The TC39 Process

So who decides what goes into JavaScript? And how does an idea become an official part of the language?

TC39 (Technical Committee 39) is the group responsible for the ECMAScript specification. It’s made up of delegates from browser makers (Google, Mozilla, Apple, Microsoft), large tech companies (Meta, Bloomberg, Salesforce), and independent contributors. They meet roughly every two months.

Every new JavaScript feature starts as a proposal and must pass through a defined set of stages before it becomes part of the language.

How Features Are Born
The TC39 Proposal Process
Every feature you use in modern JavaScript traveled this exact path before landing in the language.
0
Stage 0: Strawperson
Anyone can submit an idea at this stage. No formal requirements. It’s just a concept someone thinks could be useful. Most ideas live here briefly and never go further.
Think of it as: “Hey, what if JavaScript could do X?”
1
Stage 1: Proposal
A TC39 “champion” takes responsibility for the proposal. The problem being solved must be clearly defined, and an initial solution sketched out. TC39 has agreed the problem is worth solving.
Think of it as: “Here’s the problem, here’s a rough solution, let’s discuss.”
2
Stage 2: Draft
Initial spec text is written using formal specification language. Experimental implementations may begin. The design is mostly settled but still open to refinement. This stage can last a while.
Think of it as: “Here’s the formal spec draft. Let’s refine the details.”
3
Stage 3: Candidate
The spec is complete. Browser vendors begin shipping implementations. Real-world usage may reveal issues, but design changes are very rare at this point. If you see a Stage 3 proposal, it’s basically coming to the language.
Think of it as: “This is done. Implementations are live. Let’s catch any edge cases.”
4
Stage 4: Finished
At least two independent implementations exist, all tests pass, and the feature is ready. Stage 4 proposals are included in the next annual ECMAScript release, officially becoming part of JavaScript forever.
✓ Ships in JavaScript   It’s now part of the spec. Every browser will implement it.

This process is why JavaScript is stable and backward-compatible. Nothing gets added recklessly. Every feature goes through real debate, experimentation, and browser implementation before it’s finalized.

You can actually follow proposals on the TC39 GitHub repository. It’s genuinely interesting to see what’s in the pipeline for future versions of the language.


07What’s Landed Since ES6: A Year-by-Year Overview

Here’s a quick reference for what each ES2016-ES2024 release added. These are the features you’ll encounter most in real code:

Version Year Notable Additions
ES2016 2016
Array.includes()** exponent
ES2017 2017
async/awaitObject.values()Object.entries()String.padStart()
ES2018 2018
Object spreadPromise.finally()Async iteration
ES2019 2019
Array.flat()Array.flatMap()Object.fromEntries()
ES2020 2020
?. optional chaining?? nullish coalescingBigIntPromise.allSettled()
ES2021 2021
String.replaceAll()&&= ||= ??=Promise.any()
ES2022 2022
Class fields (#private)Array.at()Top-level awaitObject.hasOwn()
ES2023 2023
Array.toSorted()Array.toReversed()Array.findLast()
ES2024 2024
Object.groupBy()Promise.withResolvers()RegExp v flag

A few highlights worth knowing right now, even if you haven’t used them yet:

async/await (ES2017) is how almost all asynchronous code is written today. It builds on Promises from ES6 but reads like synchronous code. You’ll use this constantly.

Optional chaining ?. (ES2020) prevents the dreaded “Cannot read properties of undefined” error when accessing deeply nested object properties. user?.address?.city returns undefined instead of crashing if any part is missing.

Nullish coalescing ?? (ES2020) provides a default value when something is null or undefined, unlike || which also triggers on 0 and "".

// Optional chaining — no more crashing on undefined
const user = {
  name: "Bilal",
  address: null
};

// Without optional chaining: TypeError crash
// console.log(user.address.city);

// With optional chaining: safe, returns undefined
console.log(user?.address?.city); // undefined

// Nullish coalescing — default only for null/undefined
const settings = { volume: 0 };
const volume = settings.volume ?? 50; // stays 0
const volume2 = settings.volume || 50; // becomes 50! (wrong)

console.log(volume);  // 0 — correct, 0 is a valid value
console.log(volume2); // 50 — oops, || treated 0 as falsy

08How to Know What You Can Actually Use Today

Here’s the question that trips up a lot of developers: “I see this feature exists in ES2022. Can I use it right now without breaking anything?”

The answer is almost certainly yes for most modern features, but let’s break down what “safe to use” actually means.

Browser Support and the Can I Use Tool

Browser support for newer JavaScript features is generally excellent today. Chrome, Firefox, Safari, and Edge all update frequently and implement new ECMAScript features quickly. For native JavaScript features (not CSS), caniuse.com and MDN Web Docs show browser compatibility tables for every feature.

For example, optional chaining (?.) has been supported in all major browsers since 2020. Class private fields since 2021. Array.at() since 2022. If your users aren’t on very old browsers, you can use these freely.

Babel: When You Need Older Browser Support

If you’re building something that needs to work on older browsers, Babel is a JavaScript compiler that transforms modern JavaScript syntax into older ES5-compatible code. You write ES2022+, Babel outputs ES5 that even Internet Explorer can run.

Modern build tools like Vite and Create React App handle Babel configuration for you automatically. You rarely need to set it up manually these days.

Node.js and Server-Side JavaScript

Node.js also updates its JavaScript support as V8 (its engine) gets updated. Most ES2020 and ES2021 features are fully available in current Node.js LTS versions. If you’re writing server-side JavaScript, you generally have access to quite modern syntax.

The Quick Mental Model

Here’s a simple rule of thumb: anything in ES2015 through ES2020 is safe to use in essentially all modern browsers without Babel. For ES2021 and newer, check MDN quickly if you’re unsure. If your project already uses a build tool, you can use the latest ESNext features without worrying, because the build tool handles compatibility.


09ESNext: The “What’s Coming Next” Bucket

“ESNext” is an informal term that refers to whatever features are currently proposed or in the pipeline, but haven’t shipped in an official ECMAScript release yet. It’s a moving target by definition since every year what was “ESNext” becomes the next ES20XX release.

When you see a library or build tool mention “ESNext support,” it means they’re handling the very latest syntax, including features that might still be Stage 3 proposals being tried out in environments that already implement them experimentally.

Things like the Temporal API (a much better date/time API to replace the notoriously awkward Date object) have been in proposal stages for a while and are heading toward the language. That’s the kind of thing people mean when they say ESNext.


10A Note on Where This Fits in the Series

We covered a lot in this article, but you’ll notice that several ES6+ features like let, const, destructuring, spread/rest, and template literals got brief introductions here rather than deep dives. That’s intentional.

The next batch of this JavaScript series goes deep on every one of those topics. Variables and the var vs let vs const difference get a full dedicated article. Data types, strings, numbers, type coercion, destructuring, and the spread/rest operators each get their own thorough treatment with detailed examples and edge cases.

This article was about the big picture: where these features came from, why they exist, and how the language continues to grow. Now you have the context to make sense of everything that comes next.


11Quick Recap

ECMAScript is the specification. JavaScript is the implementation. Every version from ES1 (1997) to today has added things to the language, but none were as significant as ES6 in 2015, which brought modern syntax, modules, promises, and much more.

Since ES6, TC39 ships a new version every year. Features go through a five-stage proposal process before they land. The web platform supports most modern JavaScript features natively, and tools like Babel handle the rest for older targets.

Understanding this history means you can confidently read and write modern JavaScript, knowing exactly why it looks the way it does, and what version introduced each thing you’re using.

The next piece takes everything up a notch. We go into variables: var, let, and const, how they actually differ at a deep level, and why reaching for const by default is one of the best habits you can build as a JavaScript developer.

ES5, ES6, and the ESNext

Choose a difficulty to load your challenge

Loading challenge…

Ready to Practice?

Select a difficulty level above to load your challenge.

Easy

1
Live Preview
Great Work!