Development

ECMAScript 2021: What’s Next for JavaScript?

11 Jun 2021 7:01am, by

The next annual update to ECMAScript — the formal specification of the JavaScript language — will be coming out this July, adding all the suggested features that have reached stage 4. This means the features have been signed off by all the ECMAScript editors, shipped in at least two compatible implementations, and passed acceptance testing.

This year’s additions to JavaScript are mostly improvements to make working with the language more pleasant for developers, providing new functions or simpler ways to express something that can be done in other ways. But there’s one powerful new option that’s also a fairly fundamental change to the language, and it’s included specifically to support WebAssembly.

The New Stack asked two of the co-chairs of the TC39 committee that standardizes ECMAScript, Rob Palmer (head of Bloomberg’s JavaScript Infrastructure and Tooling team) and Brian Terlson (principal architect on the Azure SDK for TypeScript and JavaScript, and former editor of the spec) to walk us through the impact of the new features.

Promise.any

Adding asynchronous programming support to a single-threaded environment like JavaScript (the async/await feature arrived in ECMAScript 2017) meant developers could move from callbacks to promises (objects that are represent processes that are already happening but whose values won’t be known until an asynchronous operation has completed). Subsequent versions of ECMAScript have gradually added more features for handling promises

With Promise.any, JavaScript now has four ways to deal with sets of asynchronous operations to handle how promises are settled (either by being fulfilled or rejected):

  • race lets you track multiple promises and take action as soon as the first promise either succeeds or fails.
  • all only succeeds if all the promises succeed.
  • allSettled returns an array listing whether each promise succeeds or fails.
  • any fills a gap by succeeding as soon as any of the promises succeeds and failing if they all fail.

“The subtlety between these four is whether you need all of [the promises] or is one of them OK, and is a rejection fatal or should we keep going,” Palmer said.

Promise.any is most useful when you have a situation that offers some redundancy, he suggested. “Imagine you could fetch a document or a resource from multiple different websites. Maybe you want to accelerate the performance of your website by having it, in parallel, request it from multiple different servers and you don’t care which one of those requests succeeds. All you’re interested in is the result as soon as the first successful one completes.”

Promise.any won’t automatically terminate any of the other requests; you’re still responsible for manually canceling them after Promise.any returns its results.

If all the promises are rejected, Promise.any returns an aggregate error: an array of the individual errors from each promise, because you may need to know precisely why each operation failed.

Promise.allSettled introduced the aggregate error to JavaScript. Promise.any, Terlson noted, also standardizes the aggregate error to make sure any future features will use the same syntax — because it’s a very useful error type that’s already widely used in libraries (and the Azure SDK he works on).

“Promise.allSettled was the first case where we had a built-in API that could return you multiple errors and we had to figure out how to represent them, but this has been a problem for libraries for a really long time,” he said. “If you have a multistage process like trying to upload files in parallel and some of the files fail, what do you do with the errors? Well, you invent your own aggregator error.”

A future version of ECMAScript might include adding causes to the Error constructor (currently a stage three proposal) — linking a chain of errors so you can follow them back to handle the underlying issue, which will work with aggregate error.

String.prototype.replaceAll

The new replace function that operates on strings fixes another long-standing gap in JavaScript. Currently, replace only changes the first instance of a string unless you use the /G flag and write a global regexp to replace all the instances. With thousands of developers on StackOverflow looking for information on how to handle this, it was a common stumbling block.

“A lot of people probably just don’t even realize it and ship their code using replace, assume it’s going to replace all instances — and then it doesn’t,” Terlson said. “Using a regular expression might be nontrivial for certain strings. It’s pretty easy for Latin characters. But if you’ve got parens or other regex syntax in there, you can’t just replace your quotes with slashes. Now people who aren’t regexp wizards can finally replace like they wanted to all along.”

Logical Assignment Operators

JavaScript already lets you write terser code by combining mathematical operators; x += y rather than x = x + y. With Logical assignment, developers will be able to do the same thing for logical operators like logical and &&, logical or || and the nullish coalescing operator ?? (introduced in ECMAScript 2020), writing x ||= y instead of x ||(x = y).

“This is both a syntactic sugar for something that’s super common, plus it avoids the footgun with where someone passes you a but valid value like false, 0, or the empty string, but you end up overwriting it with the default,” Terlson said. “If you’re using some kind of observable framework you might end up doing a whole bunch of work that you don’t need to do because you didn’t change the value.”

That’s because logical operators in JavaScript have the property of “short-circuiting,” Palmer noted.

“If the left-hand side is truthy, we never even evaluate the right-hand side,” he said. “So when you use compound assignment, we will skip the step altogether. With the nullish coalescing operator, we can start assigning missing values.” An object property might or might not have a value; now developers can write with object property ??= and give it some kind of default value. The assignment will only happen if the object property was already empty.

That’s different from compound mathematical operators, but it’s what most people would want to do in this situation. Making it the default syntax means developers are less likely to make mistakes.

Numeric Separators

It’s much easier to make sure you’re reading a long number correctly when it’s split into logical chunks. Thanks to the introduction of BigInt in ECMAScript 2020, developers can now handle very large integers. Numeric separators are syntactic sugar to make those long numbers easier to read by breaking them up with an underscore, so you can type 1_000_000_000 rather than 1000000000 for 1,000,000,000. Underscore is already used this way in Java, Python, Perl, Ruby, Rust, Julia, Ada, and C#.

“These underscores have no semantic meaning; they are purely there for us to make our numbers beautiful,” Palmer said.

Intl.DisplayNames

Does every site need its own list of translated currencies, language, or region names to use in dropdowns for language, regions, and script pickers? Intl.DisplayNames provides standard translations for some commonly used (and translated) strings, like language names and days of the week.

“At some point, you’re going to need a drop-down for users to select language and it would be nice to be able to do that without having to manually create the translations for each of those languages, since we have the database on the computer that has the local name to the local language,” Terlson said.

Developers can write shorter, more readable HTML and JavaScript if they don’t need to include the human-readable form of languages, regions, and script display names. The browser will have less to download each time and if developers aren’t writing or copy-pasting that code (or writing code to parse the output of Intl.DateTimeFormat), they can’t make mistakes in it. That makes it easier and cheaper to do localization.

WeakRef and FinalizationRegistry

One of the big advantages of JavaScript is that developers don’t usually need to do manual memory management, so it doesn’t matter that garbage collection can differ between individual browsers and other JavaScript engines. Generally, references to objects are strongly held in JavaScript: As long as you have a reference to an object, it won’t be garbage-collected.

If you want to save memory by dropping data you no longer need to store, you have to explicitly remove strong references, like event listeners. Even the existing WeakMap and WeakSet constructs aren’t truly weak references: The new WeakRef is a true weak reference that you can use to wrap things like event listeners, so they can be garbage collected.

That’s a fundamental new capability in JavaScript that’s philosophically very different from the usual approach.

“What we love in JavaScript is that we don’t need to do the manual memory tracking; we have the garbage collector to do it for us,” Palmer said. “When we want to keep objects around, we maintain a reference; we have an object property that links to another object. When we no longer need that object, we set the property to undefined or we clear it. By releasing the reference to that object, it’s gone from our world and we trust that the JavaScript engine, sometime later, will recover the underlying memory for us.”

When you do need to do manual memory tracking with event listeners or by allocating memory in a linear buffer, JavaScript hasn’t had good constructs to handle it. “How do we have you marry up these two worlds?” “How do we let the application programmer just deal with objects and represent regular lifetimes—whilst under the hood, maybe the underlying resources need to be managed manually?”

WeakRef solves that, although at the cost of extra work: “Whenever you want to use the object, maybe to dereference it, you have to ask it, ‘Are you still there?’ You may find that it’s no longer there, if everyone else has released strong references to it.”

This also helps with handling related resources like subscriptions; having the subscription callbacks use weak references to poll the object means the subscriptions won’t accidentally keep the primary object alive when it’s no longer required.

Useful as it is, there are some strong caveats about using weak references if there is any other way to solve your problem because it can mean developers are writing code that’s no longer portable.

Palmer called it a power tool, and a feature of last resort: “This is a very advanced language feature that needs to be learned and understood because otherwise, you could cause problems for yourself.”

“In some ways, this breaks a breaks a core principle of JavaScript, which is that up until now, there has never been observable garbage collection,” he warned. “You release your references, and then you trust that it will happen sometime later, but your code itself can’t introspect and see it. Being able to observe it, to write code that does different things based on this behavior, is a little bit scary.”

So why introduce a potentially dangerous feature that was considered and rejected for ECMAScript 6 and may never be used by the vast majority of JavaScript developers? Because it improves integration with WebAssembly — the increasingly popular memory-safe, sandboxed, execution environment implemented in JavaScript engines.

Ergonomic Finalizers

That’s where finalizers come in: Rather than polling the object, developers can register a callback to receive an event when an object is garbage collected, so they can remove the resources backing it.

“That’s how we achieve both efficient resource usage — we don’t leave behind garbage — and also, from the user’s point of view, they get a nice JavaScript experience,” Palmer said.

To avoid problems, he suggests not gating important behavior behind a finalizer; depending on the JavaScript engine, finalizers may not be called immediately when garbage collection runs, they may not be called in order and, in certain situations — like a tight loop running in a high-performance situation — they may not be called at all.

“This means you shouldn’t do anything in your finalizers that can be done any other way,” Palmer warned. “If you save user data only in response to a finalizer running, you might find out that you never save that user data. This whole proposal is trying to help you do fine-grained, efficient memory recovery and clean up to make sure your program is more efficient. This is an optimization that you would layer on: this is not a place to do fundamental functionality for your app.”

Terlson noted that every time he’s thought he needed weak references, he actually didn’t, but he’s still in favor of WeakRef because of the increasing importance of WebAssembly.

“All the justification that we need,” he said, “is being able to share memory more effectively between WebAssembly and JavaScript.”

Feature image by Pixabay

A newsletter digest of the week’s most important stories & analyses.