JavaScript Beginner

Modern JavaScript ES6+ Features You Should Know

Essential ES6+ features every web developer should know: arrow functions, destructuring, template literals, async/await, and more.

KC Ramo · DjangoZen Team Mar 29, 2026 19 min read 492 views

JavaScript changed dramatically with the ES6 update and the steady improvements since, and the modern language is far more pleasant and powerful than the JavaScript of a decade ago. For anyone writing JavaScript today — for a Django front-end or anywhere else — knowing these modern features is essential, because they are what current code is written in and what makes that code clean and expressive. This tutorial covers the modern JavaScript features you should know and use.

Why modern JavaScript matters

JavaScript you encounter today, in tutorials, libraries, and codebases, is written with modern features, so understanding them is necessary just to read current code, let alone write it well. Beyond that, these features genuinely improve the language — making common tasks cleaner, reducing boilerplate, and avoiding old pitfalls. Learning them is not about chasing novelty but about working effectively with the JavaScript that exists now. Whether you are adding interactivity to a Django template or building a larger front-end, modern JavaScript is the baseline, and knowing its key features is what lets you work confidently with contemporary code rather than puzzling over unfamiliar syntax.

let and const

Modern JavaScript replaced the old way of declaring variables with two better keywords: const for values that will not be reassigned, and let for values that will. These have clearer, more predictable scoping than the old approach and signal intent — const tells a reader the value is fixed. The guidance is to use const by default and let only when you genuinely need to reassign, which makes code easier to reason about. Adopting let and const over the older keyword is the most basic step into modern JavaScript, and it immediately makes code clearer about which values change and which do not.

Block scope and the classic loop bug

The old var is scoped to the whole function, so it leaks out of if blocks and loops. let and const exist only inside the braces where they are declared, and a loop using let gets a fresh variable on every pass:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}
// var: 3, var: 3, var: 3   (one shared i)

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log("let:", j), 0);
}
// let: 0, let: 1, let: 2

const cart = ["apple"];
cart.push("bread");  // allowed: contents can change
cart = ["milk"];     // TypeError: Assignment to constant variable.

The last two lines show the most common beginner misunderstanding: const does not freeze a value. It only stops the variable from being reassigned; the array or object it points to can still be modified. Also note that reading a let or const before the line that declares it throws a ReferenceError instead of quietly giving undefined as var did, which points you straight at the ordering mistake.

Arrow functions

Arrow functions provide a concise syntax for writing functions, and they are everywhere in modern code:

const double = x => x * 2;
const add = (a, b) => a + b;

Beyond brevity, they handle the this keyword differently than regular functions in a way that avoids a common source of bugs, making them especially convenient for callbacks. They are not a complete replacement for regular functions in every case, but for the short functions passed around constantly in modern JavaScript, they are cleaner and less error-prone. Understanding arrow function syntax and their behavior is essential because they appear in nearly all modern JavaScript, particularly anywhere functions are passed as arguments.

The shapes of an arrow function

const square = x => x * x;              // one param, implicit return
const sayHi = () => console.log("Hi");  // no params: () required
const total = (a, b) => {                // braces: return is required
  return a + b;
};
const makeUser = name => ({ name, active: true }); // object: wrap in ()

const broken = (a, b) => { a + b };      // mistake: returns undefined

The this difference matters in one common case: do not write object methods as arrow functions. An arrow function has no this of its own, so { count: 1, show: () => this.count } will not see count. Use the method shorthand show() { return this.count; } for methods, and arrows for callbacks passed to setTimeout, event listeners and array methods.

Template literals

Building strings that include variables used to be awkward concatenation; template literals make it clean. Using backticks, you embed expressions directly in a string with a simple syntax, and they also support multi-line strings naturally:

const name = "World";
const greeting = `Hello, ${name}!`;

This is far more readable than joining strings with plus signs, especially as the string grows or includes several variables. Template literals are a small feature with a large impact on readability, used constantly wherever strings are constructed. Adopting them in place of concatenation immediately makes string-building code cleaner and is one of the modern features you will reach for most often.

Any expression can go inside ${ }, not only a variable name:

const price = 4.5, qty = 3;
console.log(`Total: $${(price * qty).toFixed(2)} ${qty > 2 ? "(bulk)" : ""}`);
// Total: $13.50 (bulk)

Two beginner traps: placeholders only work inside backticks, so "Hello, ${name}" in ordinary quotes prints the characters literally; and building HTML from user input with a template literal and innerHTML opens the door to cross-site scripting (XSS). For user-supplied text, set element.textContent instead, which the browser always treats as plain text.

Destructuring

Destructuring lets you extract values from arrays and objects into variables concisely, in a single readable statement rather than several. You can pull specific properties out of an object or elements out of an array directly:

const {name, email} = user;
const [first, second] = items;

This is widely used to unpack data — extracting what you need from an object returned by a function or an API. It reduces repetitive access and makes code that works with structured data much cleaner. Understanding destructuring is important because it appears throughout modern JavaScript wherever objects and arrays are used, and it is one of the features that most noticeably tidies up code that handles data.

Defaults, renaming and function parameters

const user = { name: "Ana", address: { city: "Lisbon" } };

const { role = "viewer" } = user;          // default if missing
const { name: fullName } = user;           // rename
const { address: { city } } = user;        // nested
console.log(role, fullName, city);         // viewer Ana Lisbon

function createButton({ label, color = "blue" } = {}) {
  return `${label} (${color})`;
}
console.log(createButton({ label: "Save" })); // Save (blue)

Destructuring in the parameter list lets callers pass named options in any order. The = {} at the end matters: destructuring undefined throws a TypeError, so it protects against calling the function with no argument at all. A misspelled property name, on the other hand, fails silently and just gives you undefined.

Spread and rest

The spread and rest syntax, written with three dots, are powerful tools for working with arrays and objects. Spread expands an array or object into its elements — useful for copying, combining, or passing elements as arguments — while rest collects multiple elements into one, useful for gathering function arguments or the remaining properties. They are two sides of the same syntax. These enable clean patterns for copying and merging data structures and handling variable arguments that were clumsy before. Understanding spread and rest rounds out your ability to work fluently with arrays and objects, and they appear frequently in modern code for combining and manipulating data.

const merged = [...[1, 2], ...[3], 4];            // [1, 2, 3, 4]
console.log(Math.max(...merged));                 // 4
const prefs = { ...{ theme: "light", size: 14 }, theme: "dark" };
// { theme: "dark", size: 14 }  (later properties win)

function sum(...values) {                         // rest: gather args
  return values.reduce((acc, v) => acc + v, 0);
}
console.log(sum(1, 2, 3));                        // 6

Be aware that spread makes a shallow copy. In const copy = { ...original }, nested arrays and objects are still shared, so copy.tags.push("x") also changes original.tags. When you need a fully independent copy of plain data, structuredClone(original) does a deep copy.

Default parameters

Modern JavaScript lets you give function parameters default values, used when an argument is not provided. This replaces the old pattern of manually checking for missing arguments and assigning defaults inside the function, making the function signature itself express the defaults clearly. It is a small convenience that improves both readability and robustness, since functions handle missing arguments gracefully without extra code. Default parameters are part of the collection of modern features that reduce boilerplate, letting you write functions that are concise and clear about how they behave when called with fewer arguments than they can accept.

function greet(name, greeting = "Hello") {
  return `${greeting}, ${name}!`;
}
console.log(greet("Ana"));             // Hello, Ana!
console.log(greet("Ana", undefined));  // Hello, Ana!
console.log(greet("Ana", null));       // null, Ana!

A default applies only when the argument is undefined. Passing null, 0 or an empty string counts as a real value, which surprises many beginners.

Modern array methods

Modern JavaScript provides expressive array methods that replace manual loops for common operations. Methods let you transform every element, filter to those matching a condition, reduce a collection to a single value, find an element, or check whether elements meet a condition — each in a single readable call. Writing items.filter(...) or items.map(...) is clearer and less error-prone than the equivalent loop. These methods are central to modern JavaScript, encouraging a declarative style where you express what you want rather than the mechanics of iterating. Mastering the common array methods is one of the most practically valuable parts of modern JavaScript, because working with arrays is so frequent.

A worked example

Here is the kind of list a Django view might return as JSON, processed with the most common methods:

const products = [
  { name: "Keyboard", price: 49, inStock: true },
  { name: "Monitor", price: 199, inStock: false },
  { name: "Mouse", price: 25, inStock: true },
];

const names = products.map(p => p.name);          // ["Keyboard", "Monitor", "Mouse"]
const available = products.filter(p => p.inStock); // 2 products
const value = available.reduce((sum, p) => sum + p.price, 0); // 74

const cheapFirst = available
  .toSorted((a, b) => a.price - b.price)
  .map(p => p.name);                               // ["Mouse", "Keyboard"]

Three mistakes to watch for: map(x => { x * 2 }) returns an array of undefined because braces need an explicit return; reduce on an empty array with no starting value throws, so always pass one (the 0 above); and the older sort() rearranges the original array in place, while toSorted() (available in current browsers and Node.js 20+) returns a sorted copy.

Promises and async/await

Handling operations that complete later — network requests, timers — was historically messy in JavaScript, but modern features made it clean. Promises represent a future value, and async/await lets you write asynchronous code that reads like synchronous code, awaiting results in sequence rather than nesting callbacks. This is essential for the asynchronous operations that pervade web development, especially making requests to servers. Understanding promises and async/await is necessary for any real JavaScript work involving things that take time, and they transformed one of the language's historically painful areas into one of its more pleasant, which is why they are among the most important modern features to know.

Fetching JSON from a Django view

async function loadProducts() {
  try {
    const response = await fetch("/api/products/");
    if (!response.ok) {
      throw new Error(`Server responded with ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Could not load products:", error.message);
    return [];
  }
}

Note that fetch only rejects on network failures; a 404 or 500 still resolves, which is why the code checks response.ok. The most common beginner mistake is a missing await: if you log a variable and see Promise { <pending> }, you have the promise rather than its result. Also remember that await works only inside an async function or at the top level of a module, and that forEach does not wait for async callbacks; use for...of with await, or Promise.all, instead.

Modules

Modern JavaScript has a standard module system, letting you split code across files and explicitly export and import what is shared between them. This brings real structure to JavaScript projects, replacing older ad hoc approaches with a clean way to organize code into focused files with clear dependencies. Modules let you build larger applications maintainably, each file exposing what it offers and importing what it needs. Understanding the module system is important as soon as your JavaScript grows beyond a single file, because it is how modern code is organized, and it enables the kind of structure that keeps a growing codebase comprehensible rather than a tangle of globally shared code.

// utils.js
export const TAX_RATE = 0.2;
export function formatPrice(amount) {
  return `$${amount.toFixed(2)}`;
}
export default function addTax(amount) {
  return amount * (1 + TAX_RATE);
}

// main.js
import addTax, { formatPrice } from "./utils.js";
console.log(formatPrice(addTax(10))); // $12.00

// In a Django template:
// <script type="module" src="{% static 'js/main.js' %}"></script>

Named exports are imported inside braces with their exact names; the single default export is imported without braces under any name you choose. In the browser, the script tag needs type="module", import paths need the .js extension, and modules will not load from a page opened directly as a file:// URL, so serve it through Django's development server.

Optional chaining and nullish handling

Newer JavaScript added conveniences for safely working with values that might be missing. Optional chaining lets you access nested properties without errors when something along the way is absent, returning nothing instead of throwing. The nullish coalescing operator provides a fallback value specifically when something is null or undefined. Together these handle the common situation of working with data that may have missing pieces, cleanly and without verbose checks. Understanding these additions is useful because working with data that might be incomplete — from APIs, user input, optional fields — is constant, and these features make handling it safe and concise rather than requiring defensive checks at every access.

const settings = { perPage: 0, profile: null };

console.log(settings.profile?.avatar);        // undefined, no error
console.log(settings.perPage || 25);          // 25  (wrong: 0 was valid)
console.log(settings.perPage ?? 25);          // 0   (correct)

The comparison shows why ?? exists: the older || falls back for any falsy value, including 0, "" and false, while ?? falls back only for null and undefined. Use ?. only where a missing value is normal; if something should always be there, an error is more useful than a silent undefined.

Object enhancements

Modern JavaScript improved working with objects in several ways. Shorthand property syntax lets you create objects from variables concisely, computed property names let you use dynamic keys, and the spread syntax makes copying and merging objects clean. These enhancements reduce the verbosity of object manipulation, which is frequent in JavaScript. Understanding that modern object syntax offers these conveniences helps you write cleaner code when constructing and combining objects, and helps you read the modern code that uses them. They are part of the broad set of refinements that, individually small, together make modern JavaScript noticeably more pleasant to write than its older form.

Modern iteration

Beyond array methods, modern JavaScript offers clean ways to iterate. A loop construct iterates directly over the values of an array or other iterable, reading more clearly than older index-based loops for simple iteration. Combined with the array methods for transformation and filtering, you have expressive tools for working through collections. Understanding the modern iteration options lets you choose the clearest approach for each situation — a direct value loop for simple iteration, array methods for transformation. This expressiveness in working with collections is a hallmark of modern JavaScript, replacing the clunky manual loops of older code with constructs that state intent more directly.

Adopting modern features

If you are coming from older JavaScript, adopting these features is a gradual, worthwhile shift, and modern environments support them well. You can start using them immediately in current development, and reading modern code will reinforce them. The payoff is code that is cleaner, more expressive, and in line with how the ecosystem writes JavaScript today, making libraries and examples easier to follow. Understanding that moving to modern JavaScript is both straightforward in current environments and genuinely beneficial encourages you to embrace these features fully rather than writing in an older style, which is what keeps your JavaScript readable, current, and pleasant to work with for yourself and others.

Putting it together

This small module, loaded with type="module" in a Django template containing an <ul id="list">, uses most of the features covered above:

const listEl = document.querySelector("#list");
let allProducts = [];

const formatPrice = amount => `$${amount.toFixed(2)}`;

function render(products) {
  listEl.replaceChildren();
  for (const { name, price, category } of products) {
    const li = document.createElement("li");
    li.textContent = `${name} (${category?.name ?? "Other"}) ${formatPrice(price)}`;
    listEl.append(li);
  }
}


try {
  const response = await fetch("/api/products/");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  allProducts = await response.json();
  render(allProducts);
} catch (error) {
  listEl.textContent = "Products could not be loaded.";
}

Notice const for values that never change and let for the list that is replaced, destructuring inside the for...of loop, optional chaining with ?? for a missing category, textContent so product names cannot inject HTML, and a top-level await, which is allowed because the file is a module.

Reading common error messages

MessageUsual cause
ReferenceError: x is not definedTypo, or variable used outside its block or before its declaration
TypeError: Assignment to constant variableReassigning a const; use let
TypeError: Cannot read properties of undefinedMissing data; log it, and use ?. if absence is expected
SyntaxError: Cannot use import statement outside a moduleScript tag missing type="module"
Promise { <pending> } in outputMissing await

Summary

Modern JavaScript is a far cleaner and more capable language than its older form, and knowing its key features is essential for reading and writing contemporary code. Use const and let for clearer variable declarations, arrow functions for concise callbacks with predictable behavior, and template literals for readable string building. Destructuring extracts values from objects and arrays cleanly, while spread and rest syntax elegantly copy, combine, and gather data. Default parameters reduce boilerplate, and modern array methods like map, filter, and reduce replace manual loops with expressive, declarative calls. Promises and async/await tame asynchronous operations, making network requests and other delayed work read like sequential code, and the module system brings real structure to larger projects. Together these features define how JavaScript is written today, and adopting them — whether for a Django front-end or any web work — is what lets you write code that is clean, expressive, and in step with the modern ecosystem.