Deprecated & obsolete features
Learn the deprecated and obsolete JavaScript features you meet in old code, such as fn.caller, for each and Object.observe, and what replaced them.
- 01Tell the three kinds apartSay whether an old feature is deprecated, obsolete or non-standard, and what that means for code that uses it.
- 02Read old codeRecognize
fn.caller, legacy generators,for each,Object.observeandunevalwhen you meet them. - 03Replace each oneRewrite each old feature with its modern replacement, such as rest parameters,
function*,for…ofandProxy.
Deprecated, obsolete and non-standard features
Old JavaScript code is full of features that you should not write today. Some of them still run. Some of them were removed, so the old code fails. And some of them only ever worked in one browser. This lesson shows the ones you are most likely to meet, what each one did, and what to write instead.
Three words describe these old features:
- Deprecated means "still works, but don't use it". Browsers keep it for old code, and the guides tell you to use something newer.
- Obsolete means "removed". Old syntax is now a
SyntaxError, and old functions areundefined. - Non-standard means "never in the spec". The spec is the rulebook every JavaScript engine follows. A non-standard feature works only in the engines that chose to add it.
console.log(typeof "tea".substr);
console.log(typeof Object.observe);
console.log(typeof uneval);typeof gives the type of a value as a string, and it gives "undefined" for a name or property that doesn't exist. So it is a safe way to ask "is this feature here?"
Line 1 asks about substr, an old string method. It is deprecated, but every browser still has it, so line 1 prints function. The Annex B lesson covers it and the other old features browsers must keep.
Line 2 asks about Object.observe, which Chrome had from 2014 to 2016. Line 3 asks about uneval, which only Firefox had. Both were removed, so both lines print undefined.
substr, a deprecated method that still works. Lines 2 and 3 find nothing, because Object.observe and uneval are obsolete: browsers removed them.Notice that both removed features were only ever in one browser. Websites rarely depended on them, so the browser could remove them. A feature that every browser has, and that the spec describes, almost never goes away.
Read each row of the table as: "a feature of this kind does this today, and here are some examples from this lesson."
| The kind | What it means today | Examples in this lesson |
|---|---|---|
| Deprecated | It still works, but the spec or MDN tells you not to use it in new code. | brew.caller, brew.arguments, /\a/ without the u flag |
| Obsolete | It was removed. Old syntax is a SyntaxError, and old functions are undefined. | for each…in, Object.observe, uneval |
| Non-standard | It was never in the spec. Only some engines have it. | InternalError, error.fileName, displayName |
Picture a city's buses. Some old routes still run, but a sign at the stop says the route will close and points you to a newer one. Some routes have closed: the stop sign is gone, and waiting there gets you nowhere. And one hotel runs its own shuttle that isn't on the city map at all. Only that hotel's guests can ride it.
- In real life: The city's official route map
- In JavaScript: The JavaScript spec
- In real life: A route that still runs, with a sign pointing to a newer one
- In JavaScript: A deprecated feature, such as
brew.caller - In real life: A closed route whose stop sign was taken down
- In JavaScript: An obsolete feature, such as
Object.observe - In real life: A hotel's private shuttle, not on the city map
- In JavaScript: A non-standard feature, such as
InternalError
A deprecated feature still works, but new code should use its replacement. An obsolete feature was removed, so old code that uses it fails. A non-standard feature was never in the spec, so it works only in some engines. For each one, there is a modern way to do the same job.
fn.caller and fn.arguments: a function looking at its own call
In old code, a function could look at its own call from the outside, through two properties on the function itself:
brew.calleris the function that calledbrew.brew.argumentsholds the values passed tobrewin its current call.
Both are deprecated. They still work in sloppy functions, the functions in code without "use strict". The sloppy mode lesson explains that mode.
function brew() {
return brew.caller.name;
}
function serve() {
return brew();
}
console.log(serve());Lines 1 to 3 make a function brew. Line 2 reads brew.caller, the function that called brew, and returns that function's name.
Lines 4 to 6 make serve, which calls brew on line 5. Line 7 calls serve. While brew runs, its caller is serve, so line 7 prints serve.
brew runs, brew.caller points back at the function that called it, serve. So line 2 returns the name serve, and line 7 prints it.A replay of the example code, not a view inside a JavaScript engine. The example runs for real in sloppy mode.
script
function brew() { return brew.caller.name;}function serve() { return brew();}Press Step and watch brew.caller beside line 2. It holds serve only while brew runs. Once brew returns, it is null again. This replays the example code; it isn't a view inside the JavaScript engine.
When no function called brew, there is no caller to give.
function brew() {
return brew.caller;
}
console.log(brew());Line 4 calls brew from the script itself, not from inside a function. So line 2 gives null, and line 4 prints null.
brew.arguments works the same way: it only means something while the function runs.
function total(price, tax) {
return total.arguments.length;
}
console.log(total(3, 1, 5));
console.log(total.arguments);Line 1 names two parameters, price and tax. Line 4 calls total with three values. Inside the call, line 2 reads total.arguments, which holds all three values, so line 4 prints 3.
Line 5 reads total.arguments after the call has finished. No call is running, so it prints null.
Strict code blocks both properties. A strict function has no caller or arguments of its own, so reading them finds a guard on Function.prototype, the object every function inherits from. That guard always throws a TypeError.
"use strict";
function brew() {
return brew.caller;
}
try {
brew();
} catch (error) {
console.log(error.name);
}Line 1 makes the script strict. Line 6 calls brew, and line 3 reads brew.caller, which throws. Lines 7 and 8 catch the error and print its name, TypeError.
Newer kinds of function never got these properties, even in sloppy code: methods written as brew() { … } in an object, arrow functions, classes, generators and async functions.
var shop = {
brew() {
return shop.brew.caller;
},
};
try {
shop.brew();
} catch (error) {
console.log(error.name);
}Lines 1 to 5 make an object shop with a method brew. There is no "use strict". Line 7 calls the method, and line 3 reads its caller. Methods have no caller of their own, so the guard throws, and line 9 prints TypeError.
The modern way is simple: if a function needs to know something, pass it in. For any number of values, use rest parameters, written ...extras, which collect the extra values into a real array.
function brew(from, ...extras) {
return from + " sent " + extras.length;
}
function serve() {
return brew("serve", 3, 1);
}
console.log(serve());Line 1 makes brew with a parameter from and a rest parameter extras. Line 5 passes "serve" first, then 3 and 1. So from is "serve" and extras is [3, 1]. Line 7 prints serve sent 2.
This works in strict code too, and a reader can see where every value comes from. To find out who called a function while debugging, use console.trace() or a breakpoint in your browser's developer tools.
Legacy regex syntax without the u flag
A regular expression, or regex, is a pattern that text can match, such as /tea/. Flags after the last slash change how the pattern works. The u flag turns on Unicode-aware mode, which reads the pattern with strict, modern rules. The newer v flag does too.
Without u or v, a pattern is in Unicode-unaware mode. In that mode, browsers still accept some loose, old pattern syntax. It is deprecated, and with the u flag it is a SyntaxError.
console.log(/\a/.test("a"));
console.log(/{/.test("{"));
console.log(/]/.test("]"));
console.log(/\101/.test("A"));Line 1 uses \a. That is not a real escape, so the old rules read it as the plain letter a, and line 1 prints true.
Lines 2 and 3 use { and ] alone. These characters normally have a job in a pattern, but alone they are read as plain characters, so both lines print true.
Line 4 uses \101, an old octal escape: octal 101 is 65, the character number of A. It prints true.
The loose rules can hide real mistakes. \p{L} means "any letter", but only with the u flag.
console.log(/\p{L}/.test("é"));
console.log(/\p{L}/.test("p{L}"));
console.log(/\p{L}/u.test("é"));Line 1 has no flag, so \p is just the letter p, and {L} is plain text. The pattern looks for the text p{L}, which isn't in "é", so line 1 prints false.
Line 2 tests the text "p{L}" itself, so it prints true. Line 3 adds the u flag. Now \p{L} means any letter, é is a letter, and line 3 prints true.
u flag, \p is just the letter p, and {L} is plain text. So line 1 prints false and line 2 prints true. With u, \p{L} means any letter, so line 3 prints true.With the u flag, the loose forms are errors you can see. new RegExp(text, flags) builds a pattern from text while the code runs, so its SyntaxError can be caught.
try {
new RegExp("\\a", "u");
} catch (error) {
console.log(error.name);
}
console.log(new RegExp("\\{", "u").test("{"));Line 2 builds the pattern \a with the u flag. In a string, \\ means one backslash. The pattern is rejected, and line 4 prints SyntaxError.
Line 6 builds \{ with the u flag. A backslash before { is the clear way to say "a plain brace", so it is allowed, and line 6 prints true.
A pattern written straight into the code, between slashes, is checked before anything runs.
console.log("start");
console.log(/\a/u.test("a"));Line 2 has /\a/u. The engine finds the error while reading the script, before line 1 runs. So the script is a SyntaxError, and not even start is printed.
"use strict" doesn't change any of this. Strict mode changes how code runs, not how a pattern is read.
"use strict";
console.log(/\a/.test("a"));
console.log(/{/.test("{"));Line 1 makes the script strict. Lines 2 and 3 have no flag, so the old rules still apply, and both print true.
Read each row of the table as: "this pattern means this without the u flag, and this with it."
| The pattern | Without the u flag | With the u flag |
|---|---|---|
/\a/ | Matches the letter a. | A SyntaxError: \a is not a known escape. |
/{/ and /]/ | Match the characters { and ]. | A SyntaxError: write \{ and \]. |
/\101/ | Matches A, the character with octal number 101. | A SyntaxError: there is no group number 101. |
/\p{L}/ | Matches the text p{L}. | Matches any letter, such as é. |
So add the u flag, or the v flag, to new patterns. Then \p{L} works, and a typo is an error instead of a silent wrong match.
Legacy generators, comprehensions and StopIteration
A generator is a function that can pause. Each time you ask it for the next value, it runs until the next yield, hands out that value, and waits. Before generators were standard, Firefox had its own version: any normal function with yield inside became a generator. Firefox removed this in 2018.
function count() {
yield 1;
yield 2;
}Lines 1 to 4 make a normal function with yield inside. Today, in a normal sloppy function, yield is just a name, like price. A name followed by 1 makes no sense, so line 2 is a SyntaxError.
Old Firefox code also ended its loops in an unusual way. Each next() call gave the next value itself. When there were no values left, next() threw a special object called StopIteration. Here is how old code read every value. It doesn't run anywhere today, so this box has no Run button.
var steps = count();
try {
while (true) {
console.log(steps.next());
}
} catch (error) {
if (error !== StopIteration) throw error;
}Line 1 starts the generator. Lines 3 to 5 call next() forever and print each value. When the generator runs out, next() throws StopIteration, which ends the loop.
Line 7 checks that the error really was StopIteration. Any other error is thrown again.
The standard generator, added in 2015, is marked with * after function. Its next() returns an object with two parts: value, the value, and done, which is true when it has finished.
function* count() {
yield 1;
yield 2;
}
var steps = count();
console.log(steps.next().value);
console.log(steps.next().value);
console.log(steps.next().done);Line 1 makes count a generator with function*. Line 5 starts it. Lines 6 and 7 each run it to the next yield and print 1, then 2.
Line 8 asks again. Nothing is left, so next() returns an object with done set to true, and line 8 prints true. Nothing is thrown.
console.log(typeof StopIteration);
var steps = [3, 4].values();
steps.next();
steps.next();
console.log(steps.next().done);Line 1 prints undefined, because StopIteration no longer exists. Line 2 asks an array for its values, which works like a generator. Lines 3 and 4 take the 3 and the 4. Line 5 asks once more and prints true.
Most of the time you don't call next() yourself. A for…of loop calls it for you and stops when done is true.
Firefox also had comprehensions: a short way to build a new array from an old one, inside square brackets. They were planned for the 2015 standard, then dropped.
var prices = [3, 4, 5];
var doubled = [for (p of prices) p * 2];Line 1 makes an array of prices. Line 2 was meant to double each price, but for can't appear inside square brackets today, so the script is a SyntaxError. Firefox also had a version in round brackets that made a generator; it is gone too.
var prices = [3, 4, 5];
var doubled = prices.map((p) => p * 2);
var cheap = prices.filter((p) => p < 5);
console.log(doubled.join(" "));
console.log(cheap.join(" "));Line 2 uses map, which makes a new array by calling the function on each item. Line 3 uses filter, which keeps only the items where the function returns true.
Line 4 prints 6 8 10, and line 5 prints 3 4. join(" ") puts the items into one string with spaces between them.
for each…in, let blocks and expression closures
Three more pieces of Firefox-only syntax show up in old code, often in old Firefox add-ons. Firefox removed all three by 2018. Each one is a SyntaxError today, and each one has a standard replacement.
The first is for each…in. A normal for…in loop visits the property names of an object. The old for each…in visited the values.
var cart = { tea: 3, cake: 4 };
for each (var price in cart) {
console.log(price);
}Line 1 makes a cart with two prices. Line 2 has the word each after for, which no engine accepts today, so the script is a SyntaxError, and line 3 never runs.
var cart = { tea: 3, cake: 4 };
for (var price of Object.values(cart)) {
console.log(price);
}Line 2 uses Object.values(cart), which gives an array of the values, [3, 4]. for…of visits each item of that array, so line 3 prints 3, then 4.
The second is the let block: let with round brackets made new variables that lived only in the block after them.
var price = 5;
let (price = 3) { console.log(price); }Line 1 makes a variable price with the value 5. Line 2 meant "a new price of 3, only inside these braces". In sloppy code today, let (price = 3) reads like a call to a function named let, and a brace right after a call is a SyntaxError.
var price = 5;
{
let price = 3;
console.log(price);
}
console.log(price);Lines 2 to 5 are a plain block. Line 3 makes a new price with let, which lives only inside the block, so line 4 prints 3. Line 6 is outside the block, so it reads the first price and prints 5.
The third is the expression closure: a function with no braces and no return, whose body was a single value.
var square = function (n) n * n;
console.log(square(4));Line 1 meant "a function that returns n * n". Today a function body must be inside braces, so the script is a SyntaxError, and line 2 never runs.
var square = (n) => n * n;
console.log(square(4));Line 1 makes an arrow function. With no braces after =>, it returns the value n * n. Line 2 prints 16.
Object.observe, watch and __noSuchMethod__
Programs often need to react when an object changes, for example to redraw a page when a price changes. Old browsers each had their own way to do this:
Object.observe(cart, fn)was in Chrome from 2014 to 2016. It calledfnlater with a list of the changes.cart.watch("price", fn)was in Firefox. It calledfneach timepricewas set.
Object.observe was planned for the standard, but the plan was dropped in 2015, and Chrome removed it. Firefox removed watch in 2018.
console.log(typeof Object.observe);
console.log(typeof Object.prototype.watch);
var cart = { price: 3 };
try {
cart.watch("price", function () {});
} catch (error) {
console.log(error.name);
}Lines 1 and 2 print undefined: neither function exists. Line 5 still tries to call cart.watch. Calling undefined throws, so line 7 prints TypeError.
The standard replacement is a Proxy. A Proxy is an object that stands in front of another object, called the target. You give it traps: functions that run when code reads or sets a property on the proxy.
var cart = new Proxy({ price: 3 }, {
set(target, name, value) {
console.log(name + " changed to " + value);
target[name] = value;
return true;
},
});
cart.price = 4;
console.log(cart.price);Line 1 makes a proxy whose target is { price: 3 }. Lines 2 to 6 give it a set trap. The trap gets the target, the property's name and the new value.
Line 8 sets cart.price to 4. Because cart is a proxy, the trap runs: line 3 prints price changed to 4, and line 4 stores 4 on the target. Line 5 returns true to say the change worked.
Line 9 reads cart.price. There is no get trap, so the read goes straight to the target, and it prints 4.
A replay of the example code, not a view inside a JavaScript engine. The proxy and its trap run for real.
script
set(target, name, value) { console.log(name + " changed to " + value); target[name] = value; return true; },});cart.price = 4;console.log(cart.price);Press Step to follow line 8 into the trap and back out. One difference from Object.observe: the trap runs right away, during the assignment. Object.observe reported changes later, in a batch. This replays the example code; it isn't a view inside the JavaScript engine.
Firefox had one more hook, __noSuchMethod__. If an object had it, calling a method that didn't exist ran __noSuchMethod__ instead of throwing. Firefox removed it in 2016.
var shop = {
__noSuchMethod__: function (name) {
return "no " + name;
},
};
try {
shop.checkout();
} catch (error) {
console.log(error.name);
}Lines 1 to 5 give shop a __noSuchMethod__ function. Old Firefox would have run it for line 7 and returned no checkout.
Today __noSuchMethod__ is a normal property with no special meaning. shop.checkout is undefined, calling it throws, and line 9 prints TypeError.
var shop = new Proxy({}, {
get(target, name) {
return function () {
return "no " + String(name);
};
},
});
console.log(shop.checkout());Lines 1 to 7 make a proxy with a get trap. The trap runs on every property read and gets the property's name. Here it always returns a new function.
Line 8 reads shop.checkout, so the trap returns a function. The call brackets then call that function, which returns "no " + "checkout". Line 8 prints no checkout.
String(name) on line 4 is there because a property name can also be a symbol, and adding a symbol to a string throws.
toSource() and uneval(): values written back as code
Firefox had a method toSource() on every value, and a global function uneval(). Both gave back text that looked like the code for the value. For the cart below, both gave text like (({item:"tea", price:3})).
Old code used them to save a value as text and to copy objects. Firefox removed both in 2020, and no other browser ever had them.
var cart = { item: "tea", price: 3 };
console.log(typeof cart.toSource);
console.log(typeof uneval);
console.log(JSON.stringify(cart));Line 1 makes a cart. Lines 2 and 3 print undefined, because neither feature exists.
Line 4 uses JSON.stringify, which turns plain data into text in the JSON format, and prints {"item":"tea","price":3}. JSON.parse turns that text back into an object.
JSON can't hold functions. To see the code of a function, use toString(), which every function has.
function brew(cups) {
return cups * 2;
}
console.log(brew.toString());Lines 1 to 3 make brew. Line 4 prints the function's own code, all three lines exactly as they were written, from function brew(cups) { to the closing brace.
Old code often copied an object with eval(uneval(cart)): turn it into code, then run the code to build a new object. The standard way to make a deep copy, a copy where even the inner arrays and objects are new, is structuredClone.
var cart = { item: "tea", sizes: [3, 4] };
var copy = structuredClone(cart);
copy.sizes.push(5);
console.log(cart.sizes.length);
console.log(copy.sizes.length);Line 1 makes a cart with an inner array, sizes. Line 2 makes a deep copy. Line 3 adds 5 to the copy's sizes only.
Line 4 prints 2: the original array still has two items. Line 5 prints 3. The copy got its own array, so changing it didn't change the original.
Non-standard extras: InternalError, displayName and fileName
Some features were never in the spec, yet one engine still has them today. They aren't deprecated or removed; they are just not everywhere. Code that uses them behaves differently in different browsers.
InternalError is one. Firefox throws it when its engine hits a limit, most often when a function calls itself forever. The spec says the same problem is a RangeError, which is what Chrome and Safari throw.
function down() {
return down();
}
try {
down();
} catch (error) {
console.log(error.name);
console.log(error instanceof RangeError);
}Lines 1 to 3 make down, which calls itself with no way to stop. Line 5 calls it. Each call waits for the next, until the engine runs out of room for waiting calls and throws.
Lines 7 and 8 print the error's name, and whether it is a RangeError. In Chrome, Safari and Node.js they print RangeError and true. In Firefox they print InternalError and false.
So don't check for one engine's error name. If you must catch this error, check error instanceof Error, which is true in every browser.
displayName is another. You can set it on a function to give it a friendlier name in tools. The standard name is name, which JavaScript sets for you.
var brew = function () {};
brew.displayName = "Make tea";
console.log(brew.name);
console.log(brew.displayName);Line 1 stores a function in brew. JavaScript gives the function the name brew from the variable. Line 2 sets displayName.
Line 3 prints brew, and line 4 prints Make tea. Any engine lets you set the property, but among browsers only the Firefox console shows it. React's developer tools also read displayName on components.
The last one is fileName. Firefox adds fileName, lineNumber and columnNumber to every error. Other engines don't.
var error = new Error("no tea");
console.log(typeof error.fileName);
console.log(typeof error.lineNumber);
console.log(typeof error.stack);Line 1 makes an error. In Chrome and Safari, lines 2 and 3 print undefined. In Firefox, they print string and number.
Line 4 prints string in every major browser. stack, the list of calls that led to the error, isn't in the spec yet either, but every engine has it. Its exact text is different in each one.
The safe way to use any non-standard feature is a feature check: test that it exists before you use it, and have a plan for when it doesn't.
function whereFrom(error) {
if (typeof error.fileName === "string") {
return error.fileName + ":" + error.lineNumber;
}
return "unknown";
}
console.log(whereFrom(new Error("no tea")));Line 2 checks whether fileName is a string. If it is, line 3 returns the file and the line number. If not, line 5 returns unknown.
In Chrome and Safari, line 7 prints unknown. In Firefox, it prints the address of the file and the line number where the error was made, 7. Either way, nothing throws.
Finding and replacing old features in old code
Obsolete syntax announces itself: the script fails with a SyntaxError and doesn't run at all. Deprecated features are quieter. They keep working until someone adds "use strict", turns the file into a module, or moves code into a class. Here is an old helper that logs its calls.
function logCall() {
var from = logCall.caller ? logCall.caller.name : "top";
return from + " sent " + logCall.arguments.length;
}
function checkout() {
return logCall("tea", 3);
}
console.log(checkout());
console.log(logCall());Line 2 reads logCall.caller. If a function called logCall, it takes that function's name; if not, it uses "top". Line 3 counts the values passed in, with logCall.arguments.length.
Line 6 calls logCall from checkout with two values, so line 8 prints checkout sent 2. Line 9 calls it from the script itself with no values, so it prints top sent 0.
"use strict";
function logCall(from, ...values) {
return from + " sent " + values.length;
}
function checkout() {
return logCall("checkout", "tea", 3);
}
console.log(checkout());
console.log(logCall("top"));Line 1 makes the script strict, to show this version is allowed there. Line 2 takes the caller's name as the first parameter, from, and collects the other values with the rest parameter ...values.
Line 6 passes "checkout" itself. Line 9 passes "top". Lines 8 and 9 print the same two lines as before.
Before you rewrite, sort what you find. Deprecated features still run, so you can replace them one at a time. Obsolete ones already fail. Non-standard ones work only in some browsers, so check them first.
brew.callerbrew.arguments/\a/with no flagfor each (var p in cart)let (price = 3) { … }function count() { yield 1; }Object.observe(cart, fn)uneval(cart)InternalErrorerror.fileNamebrew.displayName = "Make tea"
Put each piece of old code in its group: deprecated and still running, obsolete and removed, or non-standard and only in some engines.
The playground runs an old feature and its modern replacement side by side, for real, in this browser. Pick a feature, guess what the old program does, then add "use strict" on top and guess again.
function brew() {
return brew.caller.name;
}
function serve() {
return brew();
}
console.log(serve());function brew(from) {
return from;
}
function serve() {
return brew("serve");
}
console.log(serve());sameserveserveBoth programs print the same thing. The old feature is deprecated: it still works, but write the modern one in new code.
Try fn.caller: both programs print serve, until you add "use strict" and the old one throws a TypeError. Try for each…in: the old program is a SyntaxError in both modes. Try uneval(): the old program throws a ReferenceError, because uneval doesn't exist.
The table lists every replacement in this lesson. Read each row as: "old code like this can be written like that, and here is why the new version is better."
| Old code | What to write instead | Why it is better |
|---|---|---|
brew.caller | Pass what brew needs as a parameter. | It works in strict code, and the reader can see where the value comes from. |
brew.arguments | Rest parameters: function brew(...values). | values is a real array, and it works in strict code. |
/\a/ and /{/ with no flag | /a/u and /\{/u | With the u flag, a mistake in a pattern is a SyntaxError you can see. |
function count() { yield 1; } | function* count() { yield 1; } | The * marks a generator. Without it, the code is a SyntaxError today. |
StopIteration | Check next().done, or use for…of. | next() returns { value, done } instead of throwing at the end. |
[for (p of prices) p * 2] | prices.map((p) => p * 2) | map and filter work in every engine. |
for each (var p in cart) | for (var p of Object.values(cart)) | for…of with Object.values visits each value. |
let (price = 3) { … } | { let price = 3; … } | A normal block with let inside does the same job. |
function (n) n * n | (n) => n * n | An arrow function is the short form every engine knows. |
Object.observe and cart.watch | A Proxy with a set trap | The trap sees each change as it happens, in every engine. |
__noSuchMethod__ | A Proxy with a get trap | The get trap runs for every property read, including missing methods. |
cart.toSource() and uneval(cart) | JSON.stringify(cart) and structuredClone(cart) | They are standard, and they don't need eval to turn text back into a value. |
error.fileName and error.lineNumber | error.stack, after checking it exists | Every major engine has stack. Only Firefox has fileName. |
InternalError | Check error instanceof Error | Chrome and Safari throw a RangeError for the same problem. |
A linter can find some of these for you. ESLint, a popular one, has rules such as no-caller for arguments.callee and arguments.caller, and require-unicode-regexp for regexes with no u flag.
Common mix-ups
Most mix-ups come from treating "deprecated" as "gone", or from expecting strict mode to change things it doesn't touch.
- "Deprecated means it no longer works." No. A deprecated feature still runs. Obsolete means removed.
- "
brew.calleralways throws." No. It works in sloppy functions. Only strict functions, methods, arrow functions, classes, generators and async functions throw. - "
"use strict"makes regexes strict too." No. Only theuorvflag turns on the strict pattern rules. - "
/\p{L}/matches any letter." No. Only with theuorvflag. Without one, it matches the textp{L}. - "A modern generator throws when it runs out." No. That was old Firefox's
StopIteration. Todaynext()returnsdone: true. - "
Object.observeis the standard way to watch an object." No. It was dropped and removed. Use a Proxy with asettrap. - "
InternalErroris a standard error type." No. Only Firefox has it. The spec usesRangeError. - "
error.stackis in the spec." No. Every engine has it, but its text is different in each one.
The table below puts three of these side by side. Read each row as: "people often think this about the feature, but this is true."
| The feature | What people think | What is true |
|---|---|---|
brew.caller | It is gone from JavaScript. | It still works in sloppy functions. Strict functions, methods and arrow functions throw a TypeError. |
"use strict" | It turns on the strict regex rules. | Only the u or v flag does. /\a/ still works in strict code. |
InternalError | It is a normal error type. | Only Firefox has it. Other engines throw a RangeError when a function calls itself forever. |
Practice exercises
Every program below is a sloppy classic script unless its line 1 says "use strict". For each one, find the old feature first and ask: is it deprecated, obsolete or non-standard? Guess first, then run it.
serve calls order, and order calls brew. What does line 10 print? Then rewrite the program without caller.
function brew() {
return brew.caller.name;
}
function order() {
return brew();
}
function serve() {
return order();
}
console.log(serve());function brew(from) {
return from;
}
function order() {
return brew("order");
}
function serve() {
return order();
}
console.log(serve());brew.caller is the function that called brew directly, which is order, so line 10 prints order. The version shown passes the name in as a parameter instead, so it also prints order and works in strict code.
What does line 1 print? Then write one pattern, with the u flag, that matches the text p{N}.
console.log(/\p{N}/.test("5"), /\p{N}/u.test("5"));console.log(/p\{N\}/u.test("p{N}"));
console.log(/\p{N}/u.test("5"));The first pattern has no flag, so it looks for the text p{N}, and "5" doesn't contain it: false. The second has u, so \p{N} means any number character, and 5 is one: true. Line 1 prints false true. The version shown writes both meanings clearly with the u flag: /p\{N\}/u for the text, and /\p{N}/u for any number.
What does the program print? Then fix track so that it prints 2 and the script stays strict.
"use strict";
function track(price) {
return track.arguments.length;
}
try {
console.log(track(3, 4));
} catch (error) {
console.log(error.name);
}"use strict";
function track(...prices) {
return prices.length;
}
console.log(track(3, 4));Reading track.arguments in a strict function throws a TypeError, so the catch prints TypeError. The version shown collects the values with ...prices, a real array, and prints 2.
This loop was written for old Firefox. What happens when you run it today? Then rewrite it so it prints 3 and 4.
var cart = { tea: 3, cake: 4 };
for each (var price in cart) {
console.log(price);
}var cart = { tea: 3, cake: 4 };
for (var price of Object.values(cart)) {
console.log(price);
}for each is obsolete, so the program is a SyntaxError and prints nothing. The version shown visits Object.values(cart) with for…of and prints 3, then 4.
This is how you would watch a cart today, instead of Object.observe. What does line 12 print?
var log = [];
var cart = new Proxy({ price: 3 }, {
set(target, name, value) {
log.push(name + "=" + value);
target[name] = value;
return true;
},
});
cart.price = 4;
cart.tax = 1;
cart.price += 1;
console.log(log.join(" "));var log = [];
var cart = new Proxy({ price: 3 }, {
set(target, name, value) {
log.push(name + "=" + value);
target[name] = value;
return true;
},
});
cart.price = 4; // log: price=4
cart.tax = 1; // log: price=4 tax=1
cart.price += 1; // reads 4, then sets 5
console.log(log.join(" "));Line 9 sets price to 4, line 10 sets tax to 1, and line 11 reads 4 and sets price to 5. Each assignment runs the trap once, so line 12 prints price=4 tax=1 price=5.
This proxy replaces __noSuchMethod__, but it has a bug. What does line 13 print? Then fix the trap so shop.tea() prints tea ready and shop.cake() still prints no cake.
var menu = {
tea() {
return "tea ready";
},
};
var shop = new Proxy(menu, {
get(target, name) {
return function () {
return "no " + String(name);
};
},
});
console.log(shop.tea());
console.log(shop.cake());var menu = {
tea() {
return "tea ready";
},
};
var shop = new Proxy(menu, {
get(target, name) {
if (name in target) return target[name];
return function () {
return "no " + String(name);
};
},
});
console.log(shop.tea());
console.log(shop.cake());The trap always returns a stand-in, so even shop.tea() prints no tea. The version shown returns the real method when name in target is true, so line 14 prints tea ready, and only missing methods such as cake get the stand-in: no cake.
Check your understanding
For each question, first ask what kind of feature it is: deprecated, obsolete or non-standard. Then ask whether the code is sloppy or strict. Each answer explains why it's right or wrong.
Question 1 of 7What does brew.caller give when the script itself calls brew?
Read the code, then predictfunction brew() { return brew.caller; } console.log(brew());Choose an answer to see the explanation.
Question 2 of 7Which of these still runs in Chrome today?
Choose an answer to see the explanation.
Question 3 of 7What does a regex with \q print in a strict script?
Read the code, then predict"use strict"; console.log(/\q/.test("q"));Choose an answer to see the explanation.
Question 4 of 7How does a modern generator say it has finished?
Read the code, then predictfunction* count() { yield 3; } var steps = count(); steps.next(); console.log(steps.next().done);Choose an answer to see the explanation.
Question 5 of 7What replaced Object.observe and watch for reacting to changes on an object?
Choose an answer to see the explanation.
Question 6 of 7What happens when old code calls uneval today?
Read the code, then predicttry { uneval({ price: 3 }); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 7 of 7What does it mean that InternalError is non-standard?
Choose an answer to see the explanation.
Key takeaways
Old code will keep showing you these features. Knowing which kind each one is tells you whether it still runs, and what to write instead.
- Deprecated features still run, obsolete ones were removed, and non-standard ones were never in the spec.
fn.callerandfn.argumentswork only in sloppy functions. Pass values as parameters, and use rest parameters.- Without the
uorvflag, patterns like/\a/still work. Adduto turn mistakes into errors and to use\p{…}. - Legacy generators, comprehensions,
for each, let blocks and expression closures are SyntaxErrors today. Writefunction*,map,for…of, a block withlet, and arrow functions. Object.observe,watchand__noSuchMethod__are gone. A Proxy withsetandgettraps does their jobs.toSourceandunevalare gone. UseJSON.stringify,toStringandstructuredClone.InternalError,displayNameandfileNameare non-standard. Check that a feature exists before you use it.
Remember the one-liner.
Deprecated still runs, obsolete is gone, non-standard is only somewhere; in new code, write the standard replacement.
Coming next: Don't break the web. It explains why JavaScript can add features but almost never remove the standard ones.