---
title: 'Language Versions and Standardization'
source: 'https://academia.sh/en/courses/javascript-fundamentals/language-versions-and-standardization'
course: 'JavaScript Fundamentals'
language: en
updated: '2026-08-23T07:00:57+00:00'
license: 'CC BY-SA 4.0'
---

# Language Versions and Standardization

How ECMAScript editions are versioned, proposal stages, the traces backward compatibility leaves in the language, and feature detection.

The previous lesson used module mode, the `const` declaration, and the `for...of` loop.
None of these existed in the language's first form; each entered the standard with a
specific edition. Whether a feature "is in the language" and whether it "works in the
environment you have" are separate questions, and their answers come from separate
places.

This lesson covers how the standard is versioned, which stages a proposal passes
through, and what lasting traces the concern for compatibility leaves in the language.
It closes with a practical technique: testing whether an ability exists without looking
at a version name.

## Versioning the Standard

The language's definition is the document numbered ECMA-262. The document is versioned
as an **edition**; an edition is a complete text containing every feature accepted into
the standard up to that point. Editions add on top of previous editions; a feature that
has been published is not removed in a later edition.

Editions moved to a yearly schedule starting with ECMAScript 2015, and from that edition
on are referred to by their publication year. This is a technical naming convention: the
sentence "as of ES2015, block-scoped declarations exist" says which text defines them.
An edition's name says nothing about where that feature can be used; that question
belongs to the environment.

Version names will be used in this course only where a behavior's origin needs to be
shown. For instance, the sentence "block scope arrived with ES2015" explains why the
difference between `var` and `let` exists.

## How a Feature Enters the Language

Proposals go through a staged process in the technical committee that maintains the
language. The stages are numbered zero to four, and each stage has an entry criterion:

- **Stage 0 — idea.** An unformatted proposal submitted to the committee.
- **Stage 1 — proposal.** The problem to be solved, example usage, and possible
  difficulties are written down. The committee has found the problem worth taking up.
- **Stage 2 — draft.** The syntax and semantics are written formally, in the standard's
  language. Changes made after this stage are at the level of detail.
- **Stage 3 — candidate.** The text is complete; the turn has come to implementation and
  usage feedback. At least one engine implements the feature.
- **Stage 4 — finished.** Independent implementations and conformance tests are ready.
  The proposal enters the next edition.

The process has two consequences. First, a feature may work in some environments before
it enters the standard; second, a proposal that has not reached stage 4 can still
change. The observation "this syntax works this way right now" does not show that the
syntax is permanent.

## The Cost of Backward Compatibility

The strongest constraint the standard carries is that previously published pages keep
working. This constraint leads even behaviors accepted as wrong to go uncorrected. It is
one of the most visible examples of the backward compatibility concept defined in the
Programming Fundamentals course's Choosing a Language lesson.

```js
console.log(typeof null);
console.log([] + {});
```

```
object
[object Object]
```

The first line's result is not the outcome of a definition but of a coding choice made
in the language's first implementation. `null` is not an object; but having `typeof
null` produce `"null"` would break code depending on this expression's result. The
second line is the type-coercion rules converting an array and an object to text and
concatenating them; it will be worked through step by step in the Type Conversion and
Coercion lesson.

These behaviors should be learned not as "oddities" but as **rules**. Each is written
explicitly in the standard and can be traced step by step. This course's Values and
Types topic reads these steps.

The second consequence of the compatibility constraint is that new abilities mostly
arrive as **additions**: the old behavior is not removed, a new path is opened beside
it. `let` and `const` arriving alongside the `var` declaration is an example of this
pattern; the old one keeps working, the new one offers a different scope rule. The
difference between the two declaration kinds is the next lesson's subject.

## Feature Detection

The question "does this environment have this ability" is not answered by looking at a
version name. How much of which edition an environment implements is learned by asking
the environment itself. For methods added to built-in objects, the probe is direct:

```js
console.log(typeof Array.prototype.map);
console.log(typeof Array.prototype.at);
console.log(typeof Object.groupBy);
```

In the environment the examples were run in, all three are found:

```
function
function
function
```

The same probe produces `undefined` in an environment that lacks the ability. This is
the basis of writing fallback code:

```js
const temperatures = [21.5, 19.75, 23, 18.25];

function lastElement(array) {
  if (typeof array.at === "function") {
    return array.at(-1);
  }
  return array[array.length - 1];
}

console.log(lastElement(temperatures));
```

```
18.25
```

Doing the probe with `typeof` matters: looking directly at an undeclared name throws a
`ReferenceError`, while `typeof` produces `"undefined"` without error. Object properties
carry no such risk, but a uniform habit prevents the wrong kind of probe.

## Filling a Missing Ability

Detection reports an absence; it does not fix it. Code that stands in for a missing
ability is called a **polyfill**. A polyfill should be defined only if the ability is
missing, and it must not overwrite an existing implementation.

```js
const builtinExists = typeof Array.prototype.at === "function";
console.log("at built-in:", builtinExists);

if (typeof Array.prototype.last !== "function") {
  Object.defineProperty(Array.prototype, "last", {
    value: function () { return this[this.length - 1]; },
    writable: true, configurable: true, enumerable: false,
  });
}
console.log([3, 7, 9].last());

const keys = [];
for (const k in [1, 2]) keys.push(k);
console.log("for...in keys:", keys);
```

```
at built-in: true
9
for...in keys: [ '0', '1' ]
```

The third line shows why the polyfill is not written as a plain assignment. A property
added to the prototype by plain assignment becomes enumerable and gets caught up in
`for...in` traversal; the definition here gives `enumerable: false`, so the array's own
keys stay unchanged.

Two rules follow. First, if a built-in ability exists, it is left untouched — a polyfill
whose behavior differs from the standard breaks the assumptions of later code. Second,
adding a non-standard name (like `last`) carries a risk of colliding with the standard;
if the same name later enters it, the two definitions come face to face.

## Why Syntax Cannot Be Detected

A method probe only works for things that **exist at runtime**. New syntax cannot be
tested this way, because the entire file is parsed before it runs. As seen in the first
lesson, a parsing error runs none of the file's lines. In an environment that does not
recognize a piece of syntax, the file containing that syntax is rejected in its
entirety; a check written inside it never gets the chance to run either.

This is why there are two separate tool classes for closing a missing ability:

- A **transpiler** converts new syntax into old syntax. It solves the parsing problem by
  changing the source.
- A **polyfill** defines a missing built-in method at runtime. It cannot change syntax;
  it only adds missing objects and methods.

The two solve different problems and cannot substitute for each other. This course uses
neither; its examples are written with syntax that can be run directly.

## Reading the Standard Itself

The standard's text is not a teaching book; it is an exact definitional document. Once
its structure is learned, it gives an indisputable answer to the question "why did this
expression produce this result." In the text, every operator and every built-in method
reduces to a call of an **abstract operation** defined by numbered steps. The addition
operator, for example, first calls an abstract operation that converts both operands to
a primitive value; whether the result is a numeric sum or a text concatenation is
decided based on that step's result.

This course's Values and Types topic will follow these steps. The goal is not to
memorize the result, but to be able to follow the steps that produce it. Once followed
once, the result of an expression not yet encountered can be found by the same method.

## Summary

- The language's definition is the ECMA-262 document; editions are cumulative and
  published features are not removed.
- Proposals go through five stages numbered zero to four; a proposal that reaches stage
  4 enters the next edition.
- Backward compatibility leads even behaviors accepted as wrong to be preserved;
  `typeof null` producing `"object"` is an example of this.
- New abilities are mostly added beside the old one rather than in its place; `let` and
  `const` existing alongside `var` is this pattern.
- A built-in ability's existence is tested with a `typeof` probe; syntax, being
  evaluated at the parsing stage, cannot be tested at runtime.

## Next Step

The declaration kinds standing side by side was this lesson's closing observation. The
next lesson compares these three declarations: the difference between `var`, `let`, and
`const` is not merely a matter of writing style — it determines which region a name is
seen in, when it becomes accessible, and whether it can be rebound. The measurement
script will be brought into a form where this difference can be observed.
