← Back to Lessons

Modern JavaScript Fundamentals

Variables: forget about var
Objects and arrays: less code, more clarity

If you're coming from another programming language and starting with JavaScript, you've probably noticed that the ecosystem has changed a lot in recent years. What you used to see in old tutorials (var, nested callbacks, string concatenation with +) is no longer how JavaScript is written today.

This article covers the modern tools and patterns that are actually used in current projects. It's not an exhaustive list of features, but rather the things that show up every day in production code.

Variables: forget about var

When JavaScript was young, everyone used var. But var has strange scoping behaviors that can cause subtle bugs. Nowadays, we use const and let. The rule is simple: use const by default. Only switch to let when you really need to reassign the variable. This makes your code more predictable and easier to reason about.

1const apiUrl = 'https://example.com'; 2const user = { name: 'Ana', age: 28 }; 3 4// This will fail 5// apiUrl = 'another-url'; 6 7// But you can modify object properties 8user.age = 29; // This is fine

At first, it might seem odd to use const for objects you modify, but remember: const means the reference doesn't change, not that the object is immutable.

Arrow functions: more than just pretty syntax

Arrow functions aren't just a shorter way to write functions. The real difference is how they handle this, which can save you a lot of headaches.

1// Before 2function double(n) { 3 return n * 2; 4} 5 6// Now 7const double = n => n * 2;

Arrow functions are used almost everywhere: in array callbacks, promises, event handlers. The only exception is when you need this to refer to the object containing the method.

1const numbers = [1, 2, 3, 4]; 2const doubled = numbers.map(n => n * 2);

This pattern is so common that it quickly becomes second nature.

Objects and arrays: less code, more clarity

Modern JavaScript has features that make working with objects and arrays much cleaner.

Shorthand properties

If you're creating an object and the variable names match the property names, you can omit the repetition:

1const name = 'Carlos'; 2const age = 30; 3 4// Before 5const user = { name: name, age: age }; 6 7// Now 8const user = { name, age };

This might seem like a small detail, but when you're building lots of objects, it makes a real difference in readability.

Destructuring: extract what you need

Destructuring is one of those features that, once you start using, you can't live without. It lets you extract values from objects and arrays elegantly:

1const user = { 2 name: 'Ana', 3 age: 28, 4 email: 'ana@example.com' 5}; 6 7// Extract only what you need 8const { name, age } = user; 9 10// Or rename if there are conflicts 11const { name: userName } = user;

With arrays, it's just as useful:

1const colors = ['red', 'green', 'blue']; 2const [first, second] = colors;

This is especially useful in function parameters. Instead of receiving a whole object and accessing its properties inside the function, you can destructure directly in the parameters:

1// Before 2function greet(user) { 3 console.log(`Hello, ${user.name}`); 4} 5 6// Now 7function greet({ name }) { 8 console.log(`Hello, ${name}`); 9}

Array methods: map, filter, reduce

If you come from functional languages, these will be familiar. If not, they might seem strange at first, but once you understand them, they make your code much cleaner.

map transforms each element:

1const numbers = [1, 2, 3, 4]; 2const doubled = numbers.map(n => n * 2); 3// [2, 4, 6, 8]

filter keeps only the elements that meet a condition:

1const numbers = [1, 2, 3, 4, 5, 6]; 2const evens = numbers.filter(n => n % 2 === 0); 3// [2, 4, 6]

reduce is the most powerful but also the hardest to understand at first. It reduces an array to a single value:

1const numbers = [1, 2, 3, 4]; 2const sum = numbers.reduce((acc, n) => acc + n, 0); 3// 10

The key with reduce is understanding that you're "accumulating" something. The first argument is the accumulator, the second is the current element, and the last argument (0 in this case) is the initial value of the accumulator.

Asynchrony: from callback hell to async/await

Asynchronous operations used to be handled with callbacks. And if you needed to do several operations in sequence, you ended up with what we call "callback hell":

1getData(function(a) { 2 getMoreData(a, function(b) { 3 getEvenMoreData(b, function(c) { 4 console.log(c); 5 }); 6 }); 7});

It's hard to read and even harder to maintain. Then came Promises, which improved things:

1getData() 2 .then(a => getMoreData(a)) 3 .then(b => getEvenMoreData(b)) 4 .then(c => console.log(c)) 5 .catch(error => console.error(error));

But the real revolution was async/await. It makes asynchronous code look like synchronous code:

1async function fetchUser(id) { 2 try { 3 const response = await fetch(`https://example.com/users/${id}`); 4 const user = await response.json(); 5 return user; 6 } catch (error) { 7 console.error('Error:', error); 8 } 9}

This is much easier to read. The code does exactly what it looks like: waits for the response, converts it to JSON, and returns the user. If something fails, it's caught in the catch.

Fetch API: goodbye XMLHttpRequest

To make HTTP requests, use the Fetch API. It's much simpler than XMLHttpRequest (which you'll probably never need to use):

1async function getUsers() { 2 const response = await fetch('https://example.com/users'); 3 4 if (!response.ok) { 5 throw new Error(`Error: ${response.status}`); 6 } 7 8 return await response.json(); 9}

One important thing: Fetch doesn't reject the promise on HTTP errors (like 404 or 500). That's why you always need to check response.ok before processing the response.

Parallel operations

If you need to make multiple requests that don't depend on each other, use Promise.all to run them in parallel:

1async function fetchMultipleUsers(ids) { 2 const promises = ids.map(id => 3 fetch(`https://example.com/users/${id}`).then(r => r.json()) 4 ); 5 6 return await Promise.all(promises); 7}

This is much faster than making the requests in sequence, especially when there are many.

Modules: organize your code

In modern JavaScript, every file can be a module. You can export functions, classes, or values, and import them where you need them. There are two types of exports: named exports and default exports.

Named exports are useful when you want to export multiple things from a file:

1// utils.js 2export const PI = 3.14159; 3 4export function sum(a, b) { 5 return a + b; 6} 7 8// app.js 9import { PI, sum } from './utils.js';

Default exports are for when you have one main thing to export:

1// User.js 2export default class User { 3 constructor(name) { 4 this.name = name; 5 } 6} 7 8// app.js 9import User from './User.js';

You can use both in the same file, but it's better to keep it simple: if a file has one main thing, use default export. If it has several related utilities, use named exports.

CommonJS vs ES Modules

If you've worked with Node.js, you've probably seen require and module.exports. That's the CommonJS syntax, Node's original module system.

1// CommonJS (old) 2const fs = require('fs'); 3module.exports = myFunction; 4 5// ES Modules (modern) 6import fs from 'fs'; 7export default myFunction;

ES Modules is the official JavaScript standard and what you should use in new code. Modern Node.js supports both, but ES Modules is the future.

Template literals: headache-free strings

Before, strings were concatenated with +, and it was ugly:

1const name = 'Ana'; 2const greeting = 'Hello, ' + name + '!';

Now, use template literals with backticks:

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

You can put any JavaScript expression inside ${}:

1const price = 99.99; 2const message = `The total is: $${(price * 1.21).toFixed(2)}`;

And best of all: you can write multiline strings without weird tricks:

1const html = ` 2 <div class="card"> 3 <h2>${title}</h2> 4 <p>${description}</p> 5 </div> 6`;

This is especially useful when generating HTML or SQL queries (though for SQL you should use prepared statements for security).

Modern operators that make life easier

Spread operator (...)

The spread operator is like "unpacking" an array or object:

1const arr1 = [1, 2, 3]; 2const arr2 = [4, 5, 6]; 3const combined = [...arr1, ...arr2]; 4// [1, 2, 3, 4, 5, 6]

With objects, it's super useful for creating copies with modifications:

1const user = { name: 'Ana', age: 28 }; 2const updatedUser = { ...user, age: 29 }; 3// { name: 'Ana', age: 29 }

Optional chaining (?.)

This operator saves you from writing so much defensive code. Before, if you wanted to access a nested property that might not exist, you had to do this:

1const city = user && user.address && user.address.city;

Now:

1const city = user?.address?.city;

If any part of the chain is null or undefined, the whole expression returns undefined instead of throwing an error.

Nullish coalescing (??)

This is different from the OR operator (||). OR returns the right side if the left is any "falsy" value (including 0, '', false). Nullish coalescing only returns the right side if the left is null or undefined:

1const value = 0; 2 3console.log(value || 10); // 10 (because 0 is falsy) 4console.log(value ?? 10); // 0 (because 0 is not null/undefined)

This is especially useful for default values:

1const port = process.env.PORT ?? 3000;

Immutability: don't modify, create

In modern JavaScript, especially if you use React or other modern frameworks, the idea is to avoid modifying data directly. Instead, create new versions.

1// ❌ Mutation 2const user = { name: 'Ana', age: 28 }; 3user.age = 29; 4 5// ✓ Immutability 6const updatedUser = { ...user, age: 29 };

Why? Because it makes your code more predictable. When you pass an object to a function, you know that function won't modify it. This also makes it easier to track changes and debug.

With arrays, use methods that return new arrays instead of modifying the original:

1const numbers = [1, 2, 3]; 2 3// ❌ Methods that mutate 4numbers.push(4); 5numbers.sort(); 6 7// ✓ Immutable methods 8const withNew = [...numbers, 4]; 9const sorted = [...numbers].sort();

ESLint: your critical friend

ESLint is a tool that analyzes your code and tells you when you're doing something you probably shouldn't. At first, it might seem annoying, but it will save you from silly bugs.

A basic configuration includes:

  • Detecting unused variables
  • Enforcing consistency in indentation and quotes
  • Warning about problematic patterns

You don't need a super strict configuration to start. It's better to begin with eslint:recommended and adjust as needed.

Putting it all together

All these concepts might seem like a lot at first, but the truth is that once you start writing modern JavaScript, these patterns quickly become natural.

Here's a real example of how all this looks together:

1// api.js 2export const fetchUsers = async () => { 3 try { 4 const response = await fetch('https://example.com/users'); 5 if (!response.ok) throw new Error('Network error'); 6 return await response.json(); 7 } catch (error) { 8 console.error('Failed to fetch users:', error); 9 return []; 10 } 11}; 12 13// app.js 14import { fetchUsers } from './api.js'; 15 16const getActiveUsers = async () => { 17 const users = await fetchUsers(); 18 const active = users.filter(user => user.active); 19 const names = active.map(user => user.name); 20 21 return names; 22}; 23 24const displayUsers = async () => { 25 const names = await getActiveUsers(); 26 const message = `Active users: ${names.join(', ')}`; 27 console.log(message); 28};

This code uses async/await, arrow functions, implicit destructuring in map/filter callbacks, template literals, and ES6 modules. And best of all, it's easy to read and understand.

JavaScript has evolved a lot. Modern features aren't just "syntax sugar"—they really make code more maintainable, easier to reason about, and less bug-prone. You don't need to learn everything at once. Start by using const/let instead of var, arrow functions, and template literals. Then add destructuring and array methods. Async/await will come naturally when you need to handle asynchronous operations.

The important thing is to write code, make mistakes, and learn from them. Modern JavaScript is much friendlier than it was a few years ago.