JavaScript ES5 vs ES6: What Still Matters in Modern JavaScript

ES6 changed JavaScript dramatically, but modern JavaScript has moved far beyond ES2015. This practical guide explains the ES5 vs ES6 differences that still matter when reading, maintaining and modernizing real code.

Javascript ES5 vs. ES6
guide.md READY

If you’re learning JavaScript today, comparing ES5 and ES6 can feel a little strange.

ES5 was released in 2009. ES6—officially ECMAScript 2015—was released in 2015.

And JavaScript did not stop evolving in 2015.

That is why I no longer think of this topic as simply:

ES5 = old JavaScript

ES6 = modern JavaScript

A better way to think about it is:

ES5
  |
  v
ES2015 / ES6
  |
  v
ES2016
  |
  v
ES2017
  |
  v
...
  |
  v
Modern JavaScript

ES6 was a major turning point, but it was not the end of JavaScript’s evolution.

So in this guide, I’ll explain the important JavaScript ES5 vs ES6 differences, but I’ll also show which ones still matter when you encounter legacy code and which modern JavaScript features came later.

This is much more useful to me than memorizing a table of features from 2015.

Table of Contents

What Do ES5, ES6 and ECMAScript Actually Mean?

JavaScript is the language developers normally talk about.

ECMAScript is the language specification JavaScript implementations follow.

ECMAScript specification
          |
          v
JavaScript implementations
          |
     +----+----+
     |         |
     v         v
 Browsers    Node.js

ES5 means ECMAScript 5.

ES6 means ECMAScript 6, but it is also called ECMAScript 2015.

After that, the naming became much easier to follow:

ES2015
ES2016
ES2017
ES2018
ES2019
...

The ECMAScript specification continues to evolve annually. If you want the language-level reference rather than a framework tutorial, the current ECMAScript specification is the authoritative source.

Do Developers Still Need to Know ES5?

Yes—but mostly so you can read older code.

You may still encounter JavaScript that looks like this:

var self = this;

function loadUser(id, callback) {
  // ...
}

var message =
  'Hello ' + user.name;

var names = users.map(
  function(user) {
    return user.name;
  }
);

I would not normally write a new application in that style today.

But being able to understand it is useful when working with older websites, libraries, plugins or long-lived applications.

So my goal is not to teach ES5 as the recommended way to write new JavaScript.

My goal is to help you recognize the old patterns and understand what replaced them.

JavaScript ES5 vs ES6: Quick Comparison

AreaTypical ES5ES2015 / ES6
Variablesvarlet, const
FunctionsFunction expressionsArrow functions added
StringsConcatenationTemplate literals
Object/array extractionManual property accessDestructuring
Function defaultsManual checksDefault parameters
ModulesNo standard ES module systemimport / export
Async abstractionCallbacks commonPromises standardized
Iterationfor, forEachfor...of added
ClassesConstructor/prototype syntaxclass syntax

That table is useful as a starting point.

The details are where the real JavaScript knowledge starts.

1. var vs let and const

This is probably the ES5-to-ES6 change I care about most when reading old code.

ES5 commonly uses var:

var price = 100;

if (true) {
  var price = 200;
}

console.log(price);
// 200

var is function-scoped rather than block-scoped.

With let:

let price = 100;

if (true) {
  let price = 200;

  console.log(price);
  // 200
}

console.log(price);
// 100

The variable declared inside the block is a separate binding.

The same block-scoping rule applies to const.

MDN’s current documentation confirms that let, const and class declarations have block scope, unlike var. See the block scope documentation.

Should You Use let or const?

My normal starting point is:

const by default

let when the variable itself
needs to be reassigned

For example:

const userId = 10;

let status = 'pending';

status = 'paid';

One important detail:

const does not make an object immutable.

const user = {
  name: 'Jay'
};

user.name = 'Sam';

// This is allowed.

What const prevents is assigning a completely different value to the variable:

user = {
  name: 'Other'
};

// TypeError at runtime because
// the binding cannot be reassigned.

That distinction is worth understanding rather than memorizing “const means constant object.”

2. Traditional Functions vs Arrow Functions

ES5 function expressions often look like:

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

ES2015 added arrow functions:

const multiply = (a, b) => {
  return a * b;
};

For a single expression, it can be shorter:

const multiply =
  (a, b) => a * b;

But I don’t use arrow functions merely because they require fewer characters.

Arrow functions behave differently around this.

Arrow Functions Do Not Have Their Own this

This is one of the biggest differences between the two syntaxes.

Consider this timer:

const counter = {
  count: 0,

  start() {
    setInterval(() => {
      this.count++;

      console.log(
        this.count
      );
    }, 1000);
  }
};

The arrow callback uses this from the surrounding start() method.

Older code often solves the same problem using:

var self = this;

setInterval(
  function() {
    self.count++;
  },
  1000
);

When I see var self = this or var that = this in legacy JavaScript, lexical this is often the reason.

Arrow functions also do not have their own arguments object and cannot be used as constructors with new. The MDN function reference documents these differences.

Don’t Replace Every function Keyword with an Arrow

This can break code.

For example:

const user = {
  name: 'Jay',

  greet() {
    console.log(
      this.name
    );
  }
};

user.greet();

I would not rewrite the method as:

const user = {
  name: 'Jay',

  greet: () => {
    console.log(
      this.name
    );
  }
};

The second version does not receive this from the object call in the way the normal method does.

Modern syntax should make behaviour clearer, not change behaviour accidentally.

3. String Concatenation vs Template Literals

Older JavaScript commonly builds strings using +:

var name = 'Jay';
var role = 'Developer';

var message =
  'Hello ' +
  name +
  ', you are logged in as ' +
  role +
  '.';

Template literals make this easier to read:

const name = 'Jay';
const role = 'Developer';

const message =
  `Hello ${name}, you are logged in as ${role}.`;

They are also useful for multiline strings:

const message = `
Order confirmed.

Order ID: ${order.id}
Total: ${order.total}
`;

This is one of those modernizations I normally find easy to read and low-risk.

4. Manual Property Access vs Destructuring

ES5-style code might look like:

var user = {
  name: 'Jay',
  email: 'jay@example.com'
};

var name = user.name;
var email = user.email;

With destructuring:

const user = {
  name: 'Jay',
  email: 'jay@example.com'
};

const {
  name,
  email
} = user;

It becomes even more useful in function parameters:

function displayUser({
  name,
  email
}) {
  console.log(
    `${name} (${email})`
  );
}

But I don’t destructure everything automatically.

If destructuring twenty properties makes it harder to understand which object they came from, keeping:

order.customerId
order.total
order.status

may be clearer than creating several detached variables.

5. Default Parameters

An old default-value pattern looks like:

function createUser(
  name,
  role
) {
  role =
    role === undefined
      ? 'user'
      : role;

  // ...
}

Modern syntax is much clearer:

function createUser(
  name,
  role = 'user'
) {
  // ...
}

One thing to remember is that a default parameter is used when the argument is undefined.

createUser(
  'Jay',
  undefined
);

// role = 'user'

But:

createUser(
  'Jay',
  null
);

// role = null

null and undefined are not interchangeable just because both can represent missing-looking values.

6. Rest Parameters Replace arguments in Many Cases

Older code may use the special arguments object:

function sum() {
  var total = 0;

  for (
    var i = 0;
    i < arguments.length;
    i++
  ) {
    total += arguments[i];
  }

  return total;
}

Rest parameters give us a normal array:

function sum(...numbers) {
  return numbers.reduce(
    (total, number) =>
      total + number,
    0
  );
}

Now array methods such as map(), filter() and reduce() work directly.

7. Spread Syntax Makes Copies and Composition Easier

For arrays:

const existing = [
  'React',
  'Node.js'
];

const technologies = [
  ...existing,
  'TypeScript'
];

For objects, modern JavaScript also supports object spread:

const user = {
  name: 'Jay',
  role: 'user'
};

const admin = {
  ...user,
  role: 'admin'
};

The property that appears later wins when the same key occurs more than once.

The current MDN spread syntax documentation covers array, function-call and object spread behaviour.

Spread Creates a Shallow Copy

This is an important practical detail.

const original = {
  name: 'Jay',

  settings: {
    theme: 'dark'
  }
};

const copy = {
  ...original
};

copy.settings.theme =
  'light';

console.log(
  original.settings.theme
);

// light

The outer object was copied.

The nested settings object is still shared.

original
   |
   +--- settings -----+
                      |
copy                   |
   |                   |
   +--- settings ------+

I never treat:

{ ...object }

as a generic deep-clone solution.

8. ES Modules Replaced Many Custom Module Patterns

ES5 didn’t have the standardized JavaScript module syntax we use today.

Browser applications often relied on global variables, IIFEs, AMD or build tooling, while Node.js became strongly associated with CommonJS:

const userService =
  require(
    './user-service'
  );

module.exports =
  userService;

ES modules introduced standardized import and export syntax:

// user-service.js

export function getUser(id) {
  // ...
}
// app.js

import {
  getUser
} from './user-service.js';

Today, ES modules are a core part of both browser JavaScript and modern Node.js development.

The MDN JavaScript modules guide covers named exports, default exports, dynamic imports and top-level await.

9. Classes Changed Syntax, Not JavaScript’s Prototype Model

Before class syntax, constructor-style code often looked like:

function User(name) {
  this.name = name;
}

User.prototype.greet =
  function() {
    return (
      'Hello ' +
      this.name
    );
  };

With class syntax:

class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello ${this.name}`;
  }
}

I find the second form easier to read when a class-based model is appropriate.

But it is useful to remember that JavaScript did not suddenly stop being prototype-based.

The class syntax provides a cleaner abstraction over JavaScript’s object and prototype mechanisms.

10. Promises Were a Major ES6 Change

Older asynchronous JavaScript commonly relies on callbacks:

loadUser(
  userId,
  function(error, user) {
    if (error) {
      return handleError(
        error
      );
    }

    loadOrders(
      user.id,
      function(
        error,
        orders
      ) {
        // ...
      }
    );
  }
);

Promises give asynchronous work a standard composable abstraction:

loadUser(userId)
  .then(user =>
    loadOrders(user.id)
  )
  .then(orders => {
    console.log(orders);
  })
  .catch(error => {
    console.error(error);
  });

That was a major improvement.

But this is also where a simple ES5-vs-ES6 article starts becoming misleading.

async/await Is Modern JavaScript, but It Wasn’t ES6

Today, I would normally write many Promise workflows using async and await:

async function loadDashboard(
  userId
) {
  try {
    const user =
      await loadUser(userId);

    const orders =
      await loadOrders(
        user.id
      );

    return {
      user,
      orders
    };
  } catch (error) {
    console.error(error);

    throw error;
  }
}

But async/await arrived after ES2015.

This is one reason I don’t use “ES6” as a synonym for all modern JavaScript.

Promises were part of the major ES2015 transition. The language continued adding better ways to work with them afterward.

Don’t Accidentally Serialize Independent Async Work

async/await makes asynchronous code easier to read, but it does not automatically make it efficient.

This:

const profile =
  await loadProfile();

const settings =
  await loadSettings();

const notifications =
  await loadNotifications();

runs sequentially.

Profile
   |
   v
Settings
   |
   v
Notifications

If the operations are independent, this may be better:

const [
  profile,
  settings,
  notifications
] = await Promise.all([
  loadProfile(),
  loadSettings(),
  loadNotifications()
]);

I only do that when they really can run independently and the downstream system can handle the concurrency.

JavaScript Continued Evolving After ES6

This is the part I think the original ES5-vs-ES6 framing misses.

A developer reading modern JavaScript today will encounter many features that were not part of ES2015.

Let’s look at a few that I use far more often than some features people traditionally memorize for ES6 interviews.

Optional Chaining

Older defensive property access can become difficult to read:

var city;

if (
  user &&
  user.address &&
  user.address.city
) {
  city =
    user.address.city;
}

Optional chaining gives us:

const city =
  user?.address?.city;

If an intermediate value is null or undefined, the chain evaluates to undefined instead of throwing.

MDN currently marks optional chaining as widely available across modern browsers. See the optional chaining documentation.

Nullish Coalescing

Older code often uses || for defaults:

var quantity =
  input.quantity || 1;

That has a subtle problem.

If 0 is a valid value:

const quantity = 0;

console.log(
  quantity || 1
);

// 1

|| treats all falsy values as a reason to use the fallback.

Nullish coalescing only falls back for null or undefined:

const quantity = 0;

console.log(
  quantity ?? 1
);

// 0

I use this distinction frequently in configuration and API code.

Optional Chaining and Nullish Coalescing Work Well Together

const city =
  user?.address?.city ??
  'Unknown city';

This reads naturally:

Get city if it exists.

Otherwise use
"Unknown city".

That is much easier for me to understand than several nested checks.

Logical Assignment Operators

Modern JavaScript also supports operators such as:

||=
&&=
??=

For example:

config.timeout ??= 3000;

This only assigns the default when config.timeout is null or undefined.

These aren’t features I would tell a beginner to memorize on day one, but they are examples of why “ES6 equals modern JavaScript” is no longer accurate.

Top-Level await Exists in Modules

Modern JavaScript modules can also use await at the top level in supported module environments.

const config =
  await loadConfig();

startApplication(
  config
);

This is valid in a module context, but not in an ordinary classic script.

That distinction is documented in MDN’s JavaScript modules guide.

Reading Legacy ES5 Code: A Translation Guide

When I encounter older JavaScript, I mentally translate patterns like these.

Legacy patternModern code may use
varconst or let
var self = thisArrow callback where lexical this is intended
String + concatenationTemplate literals
Manual property extractionDestructuring
argumentsRest parameters
Constructor + prototypeClass syntax where appropriate
Nested callbacksPromises / async-await
Manual null checksOptional chaining where appropriate
x || defaultx ?? default when only nullish values mean missing
Globals/IIFE module patternES modules

That does not mean I perform a blind search-and-replace.

Every old pattern had behaviour. I want to understand that behaviour before modernizing the syntax.

Example: Modernizing an ES5 Function Carefully

Suppose I find this:

function getUserName(
  user
) {
  var name =
    user &&
    user.profile &&
    user.profile.name;

  return name ||
    'Anonymous';
}

A first modern version might be:

function getUserName(
  user
) {
  return (
    user?.profile?.name ??
    'Anonymous'
  );
}

But notice that I changed one behaviour.

The old version uses ||.

That means an empty string becomes:

'Anonymous'

The new ?? version preserves an empty string because it only treats null and undefined as missing.

Which behaviour is correct depends on the application’s requirements.

This is why modernizing legacy JavaScript is not merely a syntax cleanup.

Don’t Modernize Working Legacy Code Without Tests

I like modern JavaScript syntax, but “this code looks old” is not enough reason for a risky rewrite.

If the code is business-critical, I want tests around its current behaviour first.

Legacy behaviour
       |
       v
Add tests
       |
       v
Modernize code
       |
       v
Run tests
       |
       v
Verify same behaviour

That gives me confidence that replacing a callback, changing scope or simplifying a default-value expression did not introduce an unrelated bug.

Browser Support: Do We Still Need to Compile Everything to ES5?

Not automatically.

The answer should depend on the environments your application actually supports.

Modern browsers support a large amount of modern JavaScript directly.

For example, MDN currently marks features such as optional chaining and spread syntax as widely available across modern browsers.

If you’re building for current browsers, transpiling everything all the way back to old ES5 syntax can create extra output that your users may not need.

But if the application has contractual support for an older browser or embedded environment, compatibility requirements are different.

I prefer choosing targets intentionally rather than using:

"support everything forever"

as an accidental build strategy.

Transpilation and Polyfills Solve Different Problems

This is another distinction worth knowing.

A transpiler can convert newer syntax into older syntax.

const add =
  (a, b) => a + b;

could be transformed into something closer to:

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

But syntax transformation does not automatically add runtime APIs an environment does not provide.

For example, an older runtime may need a polyfill for a built-in feature.

Modern syntax
    |
    +--- transpilation


Missing runtime API
    |
    +--- polyfill

Those are related compatibility tools, but they solve different problems.

Common Modern JavaScript Mistakes I Still See

1. Replacing every var without understanding scope

Usually modernizing var is a good idea, but changing scope can expose logic that depended on function-scoped behaviour.

2. Replacing every function with an arrow

Arrow functions have lexical this. That is a behaviour difference, not merely prettier syntax.

3. Assuming const means immutable

The binding cannot be reassigned. Objects referenced by that binding can still be mutable.

4. Assuming spread performs a deep clone

Nested object references are still shared unless you explicitly clone them too.

5. Using || when 0 or an empty string is valid

Sometimes ?? expresses the intended meaning more accurately.

6. Thinking async/await makes code parallel

Each await still waits before the function continues. Independent tasks need an intentional concurrency strategy.

7. Using async callbacks with forEach and expecting the loop to await them

This commonly surprises developers:

users.forEach(
  async user => {
    await saveUser(user);
  }
);

console.log(
  'Finished'
);

The surrounding code does not wait for those callbacks to complete.

If I need sequential processing:

for (const user of users) {
  await saveUser(user);
}

If independent operations can safely run together:

await Promise.all(
  users.map(
    user =>
      saveUser(user)
  )
);

The right version depends on concurrency requirements.

What JavaScript Should a Developer Learn Today?

If I were learning JavaScript now, I would not spend weeks memorizing every difference between ES5 and ES6.

I would learn modern JavaScript first:

  • const and let
  • functions and arrow functions
  • objects and arrays
  • destructuring
  • rest and spread
  • template literals
  • modules
  • Promises
  • async/await
  • optional chaining
  • nullish coalescing
  • array methods
  • closures
  • scope
  • the event loop
  • error handling

Then I would learn enough legacy syntax to recognize older code when I encounter it.

What I Would Learn Before React or Node.js

Frameworks become much easier once the JavaScript underneath them feels normal.

Before going deep into React or Node.js, I would be comfortable with:

Variables + scope
       |
       v
Functions
       |
       v
Objects + arrays
       |
       v
map / filter / reduce
       |
       v
Destructuring
       |
       v
Modules
       |
       v
Promises
       |
       v
async / await
       |
       v
Error handling

If those concepts are unclear, framework code often looks harder than it really is because you’re learning two layers at the same time.

ES5 vs ES6 vs Modern JavaScript

EraExamples
ES5-era JavaScriptvar, constructor/prototype style, callbacks, string concatenation
ES2015 / ES6let, const, arrows, classes, destructuring, Promises, modules, template literals
Later modern JavaScriptasync/await, object rest/spread, optional chaining, nullish coalescing, newer module features and continuing annual language additions

This is the distinction I find most useful today.

ES6 was the beginning of the JavaScript style most developers recognize as modern—not the complete definition of modern JavaScript.

Should You Rewrite an ES5 Application?

Not automatically.

If the application is stable, tested and rarely changed, a complete syntax rewrite may have very little business value.

I become more interested in modernization when:

  • the code is actively maintained
  • old patterns regularly cause bugs
  • new developers struggle to understand it
  • the build system targets browsers we no longer support
  • old dependencies create security or maintenance problems
  • the modernization can be protected by good tests

I prefer incremental modernization over rewriting thousands of lines purely to make the syntax look newer.

Add tests
    |
    v
Modernize one module
    |
    v
Verify behaviour
    |
    v
Deploy
    |
    v
Continue gradually

That gives me much better control over risk.

My Practical JavaScript Checklist

When writing or reviewing modern JavaScript, these are some of the things I check:

  • Use const by default and let when reassignment is needed.
  • Understand whether an arrow or normal function has the correct this behaviour.
  • Use template literals when they improve string readability.
  • Use destructuring when it makes data access clearer, not merely shorter.
  • Remember that spread performs a shallow copy.
  • Use standardized modules for new modular JavaScript.
  • Prefer Promise/async-await workflows over deeply nested callbacks where appropriate.
  • Use optional chaining for genuinely optional property access.
  • Understand the difference between || and ??.
  • Don’t create sequential async waterfalls accidentally.
  • Know your actual browser/runtime support targets.
  • Don’t modernize legacy code without understanding and testing its existing behaviour.

Final Thoughts

Understanding ES5 and ES6 is still useful, but I would not stop there.

ES5 helps you understand older JavaScript.

ES2015 explains where many of today’s familiar patterns—let, const, arrow functions, modules, classes, destructuring and Promises—came from.

But modern JavaScript includes another decade of language evolution on top of that foundation.

That is why I wouldn’t describe JavaScript development today as simply choosing ES5 or ES6.

The useful skill is being able to read older JavaScript, understand why it was written that way, and know how I would write the same idea safely with the language available today.

Learn modern JavaScript for the code you want to write. Learn ES5 so old code doesn’t look like another language when you have to maintain it.

Continue Learning

If you’re continuing into React, Node.js or TypeScript, these guides are a good next step:

Share this guideLinkedInPost

ARTICLE TOOLKIT

Save or share this guide

Keep the reference nearby or send it to a teammate solving the same problem.

Share this guideLinkedInPost

QUALITY NOTE

Written from practical development experience and reviewed for clarity. Found an outdated step?

Report a correction →

Jaydip Barad

WRITTEN BY

Jaydip Barad

Senior full-stack developer sharing production-tested lessons from 14+ years of building backend systems, WordPress platforms and modern JavaScript applications.

Node.jsTypeScriptWordPressArchitecture
Previous guide
Next guide

THE PRACTICAL DEVELOPER LETTER

Get useful engineering lessons without the noise.

New tutorials, architecture notes and tools worth knowing—delivered occasionally.




    Occasional practical tutorials. Unsubscribe any time.