The last couple of years have been an exciting time for JavaScript programmers. The TC-39 committee that oversees the ECMAScript standard has added many new features, some of which are syntactic sugar, but several of which have propelled us into a whole new era of JavaScript programming. By itself, the async/await feature promises us a way out of what's called callback fell, the situation that we find ourselves in when nesting callbacks within callbacks. It's such an important feature that it should necessitate a broad rethinking of the prevailing callback-oriented paradigm in Node.js and the rest of the JavaScript ecosystem.
A few pages ago, you saw this:
query('SELECT * from db.table', function (err, result) { if (err) throw err; // handle errors // operate on result });
This was an important insight on Ryan Dahl's part, and is what propelled Node.js's popularity. Certain actions take a long time to run, such as database queries, and should not be treated the same as operations that quickly retrieve data from memory. Because of the nature of the JavaScript language, Node.js had to express this asynchronous coding construct in an unnatural way. The results do not appear at the next line of code, but instead appear within this callback function. Furthermore, errors have to be handled in an unnatural way, inside that callback function.
The convention in Node.js is that the first parameter to a callback function is an error indicator and the subsequent parameters are the results. This is a useful convention that you'll find all across the Node.js landscape; however, it complicates working with results and errors because both land in an inconvenient location—that callback function. The natural place for errors and results to land is on the subsequent line(s) of code.
We descend further into callback hell with each layer of callback function nesting. The seventh layer of callback nesting is more complex than the sixth layer of callback nesting. Why? If nothing else, it's because the special considerations for error handling become ever more complex as callbacks are nested more deeply.
But as we saw earlier, this is the new preferred way to write asynchronous code in Node.js:
const results = await query('SELECT * from db.table');
Instead, ES2017 async functions return us to this very natural expression of programming intent. Results and errors land in the correct location while preserving the excellent event-driven asynchronous programming model that made Node.js great. We'll see how this works later in the book.
The TC-39 committee added many more new features to JavaScript, such as the following:
- An improved syntax for class declarations, making object inheritance and getter/setter functions very natural.
- A new module format that is standardized across browsers and Node.js.
- New methods for strings, such as the template string notation.
- New methods for collections and arrays—for example, operations for map/reduce/filter.
- The const keyword to define variables that cannot be changed and the let keyword to define variables whose scope is limited to the block in which they're declared, rather than hoisted to the front of the function.
- New looping constructs and an iteration protocol that works with those new loops.
- A new kind of function, the arrow function, which is lighter in weight, meaning less memory and execution time impact.
- The Promise object represents a result that is promised to be delivered in the future. By themselves, promises can mitigate the callback hell problem, and they form part of the basis for async functions.
- Generator functions are an intriguing way to represent asynchronous iteration over a set of values. More importantly, they form the other half of the basis for async functions.
You may see the new JavaScript described as ES6 or ES2017. What's the preferred name to describe the version of JavaScript that is being used?
ES1 through ES5 marked various phases of JavaScript's development. ES5 was released in 2009 and is widely implemented in modern browsers. Starting with ES6, the TC-39 committee decided to change the naming convention because of their intention to add new language features every year. Therefore, the language version name now includes the year—for example, ES2015 was released in 2015, ES2016 was released in 2016, and ES2017 was released in 2017.
Deploying ES2015/2016/2017/2018 JavaScript code
The elephant in the room is that often JavaScript developers are unable to use the latest features. Frontend JavaScript developers are limited by the deployed web browsers and the large number of old browsers in use on machines whose OS hasn't been updated for years. Internet Explorer version 6 has fortunately been almost completely retired, but there are still plenty of old browsers installed on older computers that are still serving a valid role for their owners. Old browsers mean old JavaScript implementations, and if we want our code to work, we need it to be compatible with old browsers.
One of the uses for Babel and other code-rewriting tools is to deal with this issue. Many products must be usable by folks using an old browser. Developers can still write their code with the latest JavaScript or TypeScript features, then use Babel to rewrite their code so that it runs on the old browser. This way, frontend JavaScript programmers can adopt (some of) the new features at the cost of a more complex build toolchain and the risk of bugs being introduced by the code-rewriting process.
The Node.js world doesn't have this problem. Node.js has rapidly adopted ES2015/2016/2017 features as quickly as they were implemented in the V8 engine. Starting with Node.js 8, we were able to freely use async functions as a native feature. The new module format was first supported in Node.js version 10.
In other words, while frontend JavaScript programmers can argue that they must wait a couple of years before adopting ES2015/2016/2017 features, Node.js programmers have no need to wait. We can simply use the new features without needing any code-rewriting tools, unless our managers insist on supporting older Node.js releases that predate the adoption of these features. In that case, it is recommended that you use Babel.
Some advances in the JavaScript world are happening outside the TC-39 community.
TypeScript and Node.js
The TypeScript language is an interesting offshoot of the JavaScript environment. Because JavaScript is increasingly able to be used for complex applications, it is increasingly useful for the compiler to help catch programming errors. Enterprise programmers in other languages, such as Java, are accustomed to strong type checking as a way of preventing certain classes of bugs.
Strong type checking is somewhat anathema to JavaScript programmers, but is demonstrably useful. The TypeScript project aims to bring enough rigor from languages such as Java and C# while leaving enough of the looseness that makes JavaScript so popular. The result is compile-time type checking without the heavy baggage carried by programmers in some other languages.
While we won't use TypeScript in this book, its toolchain is very easy to adopt in Node.js applications.
In this section, we've learned that as the JavaScript language changes, the Node.js platform has kept up with those changes.