tutorials

Introduction to JavaScript: From Your First Line to Real-World Code

beginnerses6javascriptprogramming-fundamentalsweb-development
Gerald VillorenteOctober 16, 202632 min read
Introduction to JavaScript: From Your First Line to Real-World Code

This introduction to JavaScript starts with a simple fact: JavaScript runs the web. Every search box that autocompletes, every "add to cart" button, every live chat window - that's JavaScript. It also runs on servers (via Node.js), in mobile apps, and even in robots. Learn it once, and you can build almost anything.

This guide takes you from zero to genuinely productive. Every concept comes with a real-world example - the kind of code you'd actually write at work - and every snippet is small enough to type into your browser console and run right now.

How to follow along: open any web page, press F12 (or Cmd+Option+J on Mac) to open DevTools, click the Console tab, and type the examples. console.log(...) prints a value so you can see it. That one trick is 90% of beginner debugging. Each snippet is self-contained - run them one at a time, and if the console ever complains a name is "already declared," refresh the page for a clean slate.

Here's the map of where we're going:

  1. Syntax basics

  2. Variables & data types

  3. Operators, conditionals & loops

  4. Functions & arrow functions

  5. Arrays & objects (where real work happens)

  6. Scope & hoisting

  7. Closures

  8. The this keyword

  9. Prototypes & the prototype chain

  10. Classes

  11. Modern syntax (ES6+)

  12. Callbacks, promises & async/await

  13. The event loop

  14. The DOM & events

  15. JSON & fetch

  16. Error handling

  17. Libraries & the ecosystem

  18. Best practices

  19. Beyond this intro: practice roadmap

1. Syntax Basics: The Grammar of JavaScript

Every language has grammar. JavaScript's is small - five rules cover most of what you'll ever write:

// This is a comment - a note for humans, ignored by the computer.
/* This is a multi-line comment.
   Useful for longer explanations. */

console.log("Hello, world!"); // prints: Hello, world!
  • Statements are instructions, usually one per line. A semicolon ; ends a statement. (JavaScript can often guess where lines end, but writing semicolons explicitly avoids sneaky bugs - and the Prettier formatter in section 18 enforces this for you.)

  • Case matters: total, Total, and TOTAL are three different names.

  • Strings (text) can use double quotes "hi", single quotes 'hi', or backticks `hi` (backticks unlock superpowers - see Template Literals later).

  • Code blocks group statements with curly braces { ... }, used for functions, if statements, and loops.

  • Whitespace (spaces, blank lines, indentation) is for human eyes. Indent with 2 spaces per level so nesting is visible.

Real-world example - a script tag is how JavaScript ships with a web page:

<!-- index.html -->
<button id="greetBtn">Say hello</button>
<script>
  console.log("Page loaded - JavaScript is running!");
</script>

Open this in a browser, open the console, and you'll see the message. That <script> tag is the doorway between a static page and an interactive app.

2. Variables & Data Types: Labeled Boxes

A variable is a labeled box that holds a value. You create one with let or const, give it a name, and put something inside with =:

let visitorName = "Ana";   // a box labeled visitorName holding "Ana"
const siteName = "My Shop"; // const = this box is sealed; it can't be reassigned

visitorName = "Ben";  // ✅ allowed: let boxes can be refilled
// siteName = "Other"; // ❌ TypeError: Assignment to constant variable

The golden rule: use const by default, let only when you know the value will change. You'll almost never need var (the pre-2015 keyword) - it has confusing scoping rules that let and const fixed. If you see var in old tutorials, mentally translate it to let.

const seals the label, not the contents. A const array or object can still have items added or properties changed - you just can't point the label at a completely different value. This trips up every beginner once:

const cart = ["apples"];
cart.push("cheese"); // ✅ fine - same box, we just added something inside
// cart = [];        // ❌ not allowed - that's relabeling the box

The 8 data types

JavaScript has 7 primitive types (single, immutable values) plus objects (containers):

  • string ("Hello", 'Hi', `Hey` ) - text

  • number (42, 3.14, -7) - all numbers (no separate int/float)

  • boolean (true, false) - yes/no decisions

  • undefined (let x; gives x the value undefined) - "declared but never given a value"

  • null (let user = null;) - "deliberately empty" (you put nothing in on purpose)

  • bigint (9007199254740993n) - huge integers (rare at beginner level)

  • symbol (Symbol("id")) - unique identifiers (rare at beginner level)

  • object ({ name: "Ana" }, [1, 2], function(){}) - collections of data (arrays, objects, functions, dates…)

Check any value's type with typeof:

typeof "cookies";    // "string"
typeof 42;           // "number"
typeof true;         // "boolean"
typeof undefined;    // "undefined"
typeof null;         // "object" - a famous 30-year-old bug in the language. null is NOT an object; just memorize this quirk.
typeof { age: 30 };  // "object"
typeof [1, 2, 3];    // "object" - arrays are a special kind of object
typeof function () {}; // "function"

JavaScript is dynamically typed: the same box can hold text one minute and a number the next. Convenient, but it means you must keep track of what's inside.

Real-world example - modeling a customer profile with the right types:

const customer = {
  name: "Ana Reyes",      // string
  age: 29,                // number
  isMember: true,         // boolean - controls discount logic later
  middleName: null,       // null - we asked, she has none (different from undefined!)
  nickname: undefined,    // undefined - we haven't asked yet
  tags: ["vip", "early-adopter"], // object (array) - a list of labels
};

console.log(typeof customer.isMember); // "boolean"

Notice how null vs undefined carries meaning: null = "we know there's no value", undefined = "we don't know yet". Teams use this distinction in forms and APIs all the time.

3. Operators, Conditionals & Loops: Making Decisions and Repeating Work

Operators

// Arithmetic
const subtotal = 100 + 25;   // 125
const discount = subtotal * 0.1; // 12.5  (10% off)
const remainder = 7 % 3;     // 1   (% = remainder, handy for "every Nth item")

// Comparison - ALWAYS use === (triple equals), not == (double equals)
5 === 5;       // true
5 === "5";     // false - different types, correctly rejected
5 == "5";      // true - == converts types first, causing subtle bugs. Just don't use it.
5 !== "5";     // true

// Logical
const isLoggedIn = true;
const hasItems = true;
const canCheckout = isLoggedIn && hasItems; // && = AND - true only if BOTH sides are true
const nickname = "" || "Guest";             // || = OR - "Guest", because "" is falsy
const loggedIn = !false;                    // ! = NOT → true

// Ternary - a one-line if/else, everywhere in real code
const isMember = true; // pretend we read this from the customer object in section 2
const label = isMember ? "Member price: $90" : "Regular price: $100";

Falsy values - the 6 values that count as false in an if: false, 0, "" (empty string), null, undefined, NaN. Everything else is truthy. Memorize these six; they explain half of all beginner confusion.

Conditionals

// Real-world example: shipping cost rules
const orderTotal = 75;
const isMember = true;

if (orderTotal >= 100 || isMember) {
  console.log("Free shipping! 🎉");
} else if (orderTotal >= 50) {
  console.log("Flat $4.95 shipping");
} else {
  console.log("Flat $9.95 shipping");
}

// switch - cleaner when one value has many possible matches
const status = "shipped";
switch (status) {
  case "pending":
    console.log("We're packing your order.");
    break;
  case "shipped":
    console.log("Your order is on its way!"); // ← this runs
    break;
  case "delivered":
    console.log("Enjoy!");
    break;
  default:
    console.log("Unknown status - contact support.");
}

Loops

// Real-world example: receipt line items
const cart = [
  { name: "Coffee", price: 4.5 },
  { name: "Bagel", price: 3.0 },
  { name: "Juice", price: 5.0 },
];

// for...of - the friendliest loop: "do this for each item"
for (const item of cart) {
  console.log(`${item.name}: $${item.price}`);
}

// Classic for - when you need the index (position number)
for (let i = 0; i < cart.length; i++) {
  console.log(`Item ${i + 1} of ${cart.length}: ${cart[i].name}`);
}

// while - repeats until a condition flips
let attempts = 0;
while (attempts < 3) {
  console.log(`Login attempt ${attempts + 1}`);
  attempts++; // ⚠️ forget this line and the loop runs forever!
}

In modern code you'll often replace loops with array methods like .map() and .filter() (coming up in section 5). Loops still matter - they're clearer for complex logic and the only option when the number of repetitions isn't tied to an array.

4. Functions & Arrow Functions: Reusable Recipes

A function is a named set of steps you define once and run anytime. Parameters are the inputs it accepts; return is the answer it hands back. (A function without return hands back undefined.)

// Function declaration - the classic form
function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet("Ana")); // "Hello, Ana!"

// Default parameters - used when the caller omits an argument
function shippingCost(total, isMember = false) {
  if (total >= 100 || isMember) return 0;
  return total >= 50 ? 4.95 : 9.95;
}
console.log(shippingCost(30));        // 9.95
console.log(shippingCost(30, true));  // 0

Arrow functions

Arrow functions (=>) are a shorter way to write functions, introduced in ES6 (2015). You'll see them everywhere - especially as short callbacks passed into array methods and event listeners:

// Same idea, less typing. Compare:
function double(n) { return n * 2; }
const doubleArrow = (n) => n * 2; // one-liner: return is implied

console.log(doubleArrow(21)); // 42

// Zero or multiple parameters need parentheses:
const hello = () => "Hello!";
const add = (a, b) => a + b;

// Multi-line arrows need braces + explicit return:
const applyDiscount = (price, rate) => {
  const savings = price * rate;
  return price - savings;
};

Arrows aren't just shorter - they also handle this differently (section 8). Rule of thumb for now: use arrows for short callbacks, regular function for top-level named logic. Both are "real" functions; neither is better in all cases.

Real-world example - a price formatter reused across a whole store:

// One function, used on every product card, cart line, and receipt:
const formatPrice = (amount, currency = "USD") =>
  new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);

console.log(formatPrice(9.5));            // "$9.50"
console.log(formatPrice(9.5, "EUR"));     // "€9.50"
console.log(formatPrice(1234.5, "PHP"));  // "₱1,234.50"

This is why functions matter in real work: pricing logic lives in one place. When the business adds tax-inclusive display, you edit one function - not 40 product pages.

5. Arrays & Objects: Where Real Work Happens

Primitives hold single values. Arrays (ordered lists) and objects (labeled collections) hold your application's actual data: carts, users, orders, API responses. Master these two and you can read most professional code.

Objects: lockers with name labels

const user = {
  firstName: "Ana",
  lastName: "Reyes",
  email: "[email protected]",
};

console.log(user.email);      // dot notation - "[email protected]"
console.log(user["email"]);   // bracket notation - same thing, needed when the key is in a variable

user.isAdmin = false;         // add a property
user.email = "[email protected]"; // update a property
delete user.lastName;         // remove a property

Arrays: ordered cubbies, counted from 0

const tasks = ["write", "review", "ship"];
console.log(tasks[0]);     // "write" - first cubby is number 0
console.log(tasks.length); // 3

tasks.push("celebrate");   // add to the end
tasks.pop();               // remove from the end

The array methods professionals use daily

Instead of writing loops by hand, real code transforms arrays with methods. Each takes a small function and applies it to every item. Learn these five and you'll understand a huge fraction of production JavaScript:

// Real-world example: an order dashboard
const orders = [
  { id: 101, total: 45.0, status: "delivered" },
  { id: 102, total: 120.0, status: "pending" },
  { id: 103, total: 200.0, status: "delivered" },
  { id: 104, total: 15.0, status: "cancelled" },
];

// .map - transform every item (returns a NEW array)
const totals = orders.map((order) => order.total);
console.log(totals); // [45, 120, 200, 15]

// .filter - keep only matching items
const delivered = orders.filter((order) => order.status === "delivered");
console.log(delivered.length); // 2

// .find - first matching item (or undefined)
const bigOrder = orders.find((order) => order.total > 150);
console.log(bigOrder.id); // 103

// .reduce - boil the array down to ONE value (sum, average, grouping…)
const revenue = orders
  .filter((order) => order.status === "delivered")
  .reduce((sum, order) => sum + order.total, 0); // 0 = starting value
console.log(revenue); // 245

// .some / .every - yes/no questions about the collection
console.log(orders.some((o) => o.status === "pending")));   // true - anything awaiting?
console.log(orders.every((o) => o.total > 0));              // true - all totals valid?

Notice how .filter().reduce() chains: filter first, then sum what's left. Chaining small steps reads like a sentence - "from delivered orders, sum the totals" - and it's the dominant style in modern codebases.

Other handy ones to know exist: .includes() (is X in the list?), .sort(), .slice() (copy a portion), .join() (array → string), Object.keys() / Object.values() / Object.entries() (loop over an object's labels and values).

6. Scope & Hoisting: Who Can See What, and When

Scope answers: "where in the code can I use this variable?" JavaScript has three levels, nested like Russian dolls:

const storeName = "My Shop"; // GLOBAL scope - visible everywhere in the file

function checkout(cart) {
  const taxRate = 0.12; // FUNCTION scope - visible only inside checkout()

  if (cart.length > 0) {
    const message = "Processing…"; // BLOCK scope - visible only inside these { }
    console.log(storeName, taxRate, message); // ✅ inner code sees outer variables
  }
  // console.log(message); // ❌ ReferenceError - message doesn't exist out here
}
// console.log(taxRate); // ❌ ReferenceError - taxRate is locked inside checkout()

The rule is one-directional: inner scopes can read outer variables, but outer scopes can't see in. This is a feature - it keeps functions self-contained so a variable named total in one function can't accidentally break another.

Hoisting: declarations are "lifted" to the top

Before running your code, JavaScript scans it and registers declarations first. The practical effects differ by keyword - and this is exactly why let/const replaced var:

console.log(greet("Ana")); // ✅ works! Function declarations are fully hoisted.
function greet(name) {
  return `Hello, ${name}!`;
}

console.log(a); // undefined (not an error!) - var is hoisted but starts empty
var a = 5;

console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 10;

let and const live in the temporal dead zone (TDZ) between the top of their scope and their declaration line: they exist, but touching them throws an error. That's good - it turns silent undefined bugs into loud, findable errors.

🐛 The classic real-world scope bug (and its one-word fix)
// ❌ THE BUG (var leaks out of the loop):
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(`var: ${i}`), 100);
}
// prints "var: 3" three times! One shared i, already 3 when the timers fire.

// ✅ THE FIX (let gives EACH loop iteration its own i):
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(`let: ${i}`), 100);
}
// prints "let: 0", "let: 1", "let: 2" - correct!

This exact bug has bitten thousands of production apps (buttons that all submit the last row's ID, tabs that all open the last panel…). The lesson: declare loop counters with let, never var.

7. Closures: Functions That Remember

A closure is a function bundled with a memory of the variables around it at the time it was created. Even after the outer function finishes, the inner function still sees those variables.

function makeCounter() {
  let count = 0; // "private" - no code outside can touch this directly
  return () => {
    count++;
    return count;
  };
}

const clicks = makeCounter();
console.log(clicks()); // 1
console.log(clicks()); // 2 - it REMEMBERED count from last time!
console.log(clicks()); // 3

Think of it as a function carrying a backpack: when makeCounter() runs, the returned arrow function packs count into its backpack and carries it forever. Each call to makeCounter() creates a fresh backpack - clicks and another counter never share state.

Why this matters at work: closures are how JavaScript creates private state - data that can only change through approved functions. No other code can corrupt it.

Real-world example - a shopping cart whose total can't be tampered with:

function createCart() {
  const items = []; // private: hidden inside the closure

  return {
    addItem: (name, price) => items.push({ name, price }),
    getTotal: () => items.reduce((sum, item) => sum + item.price, 0),
    getCount: () => items.length,
  };
}

const cart = createCart();
cart.addItem("Coffee", 4.5);
cart.addItem("Bagel", 3.0);
console.log(cart.getTotal()); // 7.5
console.log(cart.items);      // undefined - the raw list is unreachable from outside!

You also meet closures constantly without noticing: every event handler, timer callback, and .then() callback that uses an outer variable is a closure. If you've ever written fetch(url).then(data => render(data, userId)), you've used one.

🔒 Two more closure patterns you'll see in real codebases
// 1. once() - run an expensive setup exactly one time, no matter how often it's called
function once(fn) {
  let done = false;
  let result;
  return (...args) => {
    if (!done) {
      done = true;
      result = fn(...args);
    }
    return result;
  };
}
const initAnalytics = once(() => console.log("Analytics connected"));
initAnalytics(); // "Analytics connected"
initAnalytics(); // (silent - already ran)

// 2. Function factories - stamp out customized functions
const greetIn = (greeting) => (name) => `${greeting}, ${name}!`;
const sayHi = greetIn("Hi");
const sayGoodMorning = greetIn("Good morning");
console.log(sayHi("Ana"));          // "Hi, Ana!"
console.log(sayGoodMorning("Ben")); // "Good morning, Ben!"

8. The this Keyword: "Who Called Me?"

this is JavaScript's most misunderstood keyword, but the core idea is simple: this is a pronoun - it refers to whatever object the function was called on. The confusion comes from one fact: unlike most languages, this in JavaScript is decided by how you call the function, not where you wrote it.

For beginners, three rules cover nearly everything:

// RULE 1: method call - obj.method() → this is obj (the thing before the dot)
const user = {
  name: "Ana",
  greet() {
    return `Hello, I'm ${this.name}`; // this = user
  },
};
console.log(user.greet()); // "Hello, I'm Ana"

// RULE 2: plain call - fn() → `this` is NOT user anymore, so detaching a method breaks it:
const greetFn = user.greet;
console.log(greetFn()); // ❌ broken! `this` is undefined (modules/strict mode) or the
// global window object (classic scripts) - either way, `this.name` isn't "Ana"

// RULE 3: arrow functions DON'T get their own `this` -
// they inherit it from the surrounding code where they were written.
const team = {
  name: "Checkout Squad",
  members: ["Ana", "Ben"],
  listMembers() {
    // ✅ arrow inherits `this` (= team) from listMembers:
    return this.members.map((m) => `${m} is on ${this.name}`);
  },
};
console.log(team.listMembers());
// ["Ana is on Checkout Squad", "Ben is on Checkout Squad"]

Real-world example - the button-click bug every beginner hits:

// ❌ Regular function as a callback: `this` is the BUTTON, not your object
const form = {
  email: "[email protected]",
  init() {
    document.querySelector("#saveBtn").addEventListener("click", function () {
      console.log(this.email); // undefined! `this` = the button element
    });
  },
};

// ✅ Arrow function: inherits `this` (= form) from init()
const fixedForm = {
  email: "[email protected]",
  init() {
    document.querySelector("#saveBtn").addEventListener("click", () => {
      console.log(this.email); // "[email protected]" ✅
    });
  },
};

Practical takeaway: inside callbacks (event listeners, timers, .then()), prefer arrow functions so this stays what you expect. (There are also .bind(), .call(), and .apply() for explicitly setting this - know they exist; you'll reach for them later.)

9. Prototypes & the Prototype Chain: How Objects Share Superpowers

Here's a mystery every beginner eventually notices:

const colors = ["red", "green"];
console.log(colors.map((c) => c.toUpperCase())); // ["RED", "GREEN"]
// Wait - we never taught `colors` what .map is. Where did it come from?!

Answer: every array is secretly linked to a shared object called Array.prototype that provides .map, .filter, .push, and friends. When you call colors.map(...), JavaScript looks for map on colors itself, doesn't find it, then follows the hidden link to the prototype and finds it there. That lookup chain is the prototype chain:

colors  →  Array.prototype (.map, .filter, …)  →  Object.prototype (.toString, …)  →  null (end)

Think of it as asking your parents, then grandparents: if the object itself can't answer, it asks up the chain until someone can (or the chain ends in null).

const colors = ["red", "green"];
console.log(typeof colors.map); // "function" - inherited, not owned
console.log(colors.hasOwnProperty("map")); // false - proof it came from the chain!

// Everything inherits from Object.prototype, so every value gets basics like .toString:
console.log((42).toString()); // "42"

Real-world example - shared methods without copying them onto every object:

// A prototype object holding behavior every "product card" needs:
const productMethods = {
  displayPrice() {
    return `$${this.price.toFixed(2)}`;
  },
};

const coffee = Object.create(productMethods); // coffee's prototype = productMethods
coffee.name = "Coffee";
coffee.price = 4.5;

console.log(coffee.displayPrice()); // "$4.50" - found up the chain!
// A thousand product objects can share ONE displayPrice instead of each carrying a copy.

In modern code you rarely touch prototypes directly - class syntax (next section) sets up the chain for you. But understanding the chain explains error messages ("X is not a function" = nothing up the chain answered), why all arrays share methods, and how libraries extend built-ins. One chain, endless reuse.

10. Classes: Blueprints for Objects

When your app needs many similar objects (users, products, orders), a class is the blueprint: it stamps out objects with the same shape and shared methods. Under the hood it's still prototypes - but the syntax reads cleanly.

class User {
  constructor(name, email) {
    this.name = name;   // each stamped-out object gets its own name…
    this.email = email; // …and its own email
  }

  greet() {
    return `Hello, I'm ${this.name}`; // …but all share ONE greet method
  }
}

const ana = new User("Ana", "[email protected]");
const ben = new User("Ben", "[email protected]");
console.log(ana.greet()); // "Hello, I'm Ana"
console.log(ben.greet()); // "Hello, I'm Ben"

constructor runs once per new User(...) call and sets up that instance's data. Methods like greet() live on the prototype, shared by all instances - memory-efficient by default.

Real-world example - extends lets one blueprint build on another (an Admin is a User with extras):

// (Run the User snippet above first - Admin builds on it.)
class Admin extends User {
  constructor(name, email, permissions) {
    super(name, email); // super() = "run the parent constructor first"
    this.permissions = permissions;
  }

  banUser(user) {
    return `${this.name} banned ${user.name}`;
  }
}

const boss = new Admin("Cory", "[email protected]", ["ban", "edit"]);
const troublemaker = new User("Ben", "[email protected]");
console.log(boss.greet());                // inherited from User ✅
console.log(boss.banUser(troublemaker));  // "Cory banned Ben"
console.log(boss instanceof User);        // true - an Admin IS a User
console.log(boss instanceof Admin);       // true

You'll meet classes constantly: React components (historically), Node.js servers, date libraries, ORM models (post.save()), and every new Date(), new Map(), new Error() you write. Learn constructor, methods, extends, and super() and you can read all of them.

11. Modern Syntax (ES6+): Writing Less, Saying More

ES6 (ECMAScript 2015) was JavaScript's biggest upgrade, and yearly releases since have added more. Modern codebases assume these features - here's the essential kit:

Template literals - strings with superpowers

const name = "Ana";
const items = 3;
// Backticks + ${} embed any expression - no more "+" spaghetti:
console.log(`Hi ${name}, you have ${items} items ($${items * 5} total).`); // "Hi Ana, you have 3 items ($15 total)."
// Multi-line strings just work:
const email = `Dear ${name},
Thank you for your order!`;

Destructuring - unpack objects and arrays in one line

// Real-world example: an API returns a user object; grab just what you need
const response = { id: 7, name: "Ana", email: "[email protected]", role: "admin" };
const { name, email } = response; // pick two fields, ignore the rest
console.log(name, email); // "Ana" "[email protected]"

const [first, second] = ["🥇", "🥈", "🥉"]; // array version: by position
console.log(first, second); // "🥇" "🥈"

// Destructure function parameters directly - extremely common in real code:
function sendWelcomeEmail({ name, email }) {
  return `To: ${email} - Welcome, ${name}!`;
}
console.log(sendWelcomeEmail(response));

Spread & rest - the ... triple threat

// SPREAD: expand a collection (copy/merge without mutating the original)
const cart = ["coffee"];
const updatedCart = [...cart, "bagel"]; // ["coffee", "bagel"] - cart untouched!
const defaults = { theme: "light", lang: "en" };
const prefs = { ...defaults, theme: "dark" }; // { theme: "dark", lang: "en" }

// REST: gather leftovers into one collection
const [winner, ...runnersUp] = ["Ana", "Ben", "Cory", "Dee"];
console.log(winner);    // "Ana"
console.log(runnersUp); // ["Ben", "Cory", "Dee"]

function sum(...numbers) { // any number of arguments → one array
  return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

Optional chaining & nullish coalescing - kill "cannot read property of undefined"

// Real-world example: API data with missing pieces
const order = { id: 101, customer: { name: "Ana" } };

// ?. stops safely instead of crashing:
console.log(order.customer?.name);          // "Ana"
console.log(order.shipping?.address?.city); // undefined - no crash! (shipping doesn't exist)

// ?? falls back ONLY for null/undefined (unlike ||, it keeps 0 and "" - important!)
const quantity = order.quantity ?? 1;  // order.quantity is undefined → 1
const label = "" ?? "Guest";           // "" - kept! (|| would have replaced it)

Modules - split code across files

// prices.js - export what other files may use
export const TAX_RATE = 0.12;
export function withTax(amount) {
  return amount * (1 + TAX_RATE);
}

// checkout.js - import what you need
import { withTax, TAX_RATE } from "./prices.js";
console.log(withTax(100)); // 112

Every real project is dozens of modules: user.js, cart.js, api.js… Modules (plus export default for a file's main thing) are how teams keep code organized and testable.

✨ More modern gems worth knowing by name
  • Set / Map - a collection with no duplicates (new Set([1,1,2]) → {1, 2}) and a dictionary with any key type. Reach for them when plain arrays/objects feel awkward.

  • String helpers - .includes(), .startsWith(), .trim(), .replaceAll().

  • Number helpers - Number.parseInt("42"), Number.isNaN(...).

  • ??= / ||= / &&= - assign only when it makes sense: settings.theme ??= "light".

  • Top-level await - use await directly in modules without wrapping it in a function (see section 12).

12. Callbacks, Promises & Async/Await: Handling Work That Takes Time

Some work is instant (adding numbers). Some takes time: fetching data from a server, reading a file, waiting for a timer. JavaScript refuses to freeze the whole page while waiting - instead it says "go on without me, I'll call you back." Three generations of syntax handle this:

Generation 1: Callbacks - "call this function when you're done"

// setTimeout waits 1 second, THEN runs the callback. Code after it runs immediately:
console.log("Starting…");
setTimeout(() => console.log("…one second later!"), 1000);
console.log("Not waiting around!");
// Output order: "Starting…" → "Not waiting around!" → "…one second later!"

Callbacks work, but chaining several ("fetch user, then their orders, then each order's items…") nests deeper and deeper - the infamous callback hell (or "pyramid of doom"). Hard to read, harder to debug.

Generation 2: Promises - "I promise I'll have a value later"

A Promise is an object representing a future value. It starts pending, then becomes fulfilled (with data) or rejected (with an error). You attach .then() for success and .catch() for failure - and crucially, they chain flat instead of nesting:

// Real-world example: load a user, then their orders - flat chain, one .catch
// (api.example.com is a placeholder - swap in jsonplaceholder.typicode.com to run it.)
fetch("https://api.example.com/users/7")
  .then((response) => response.json())       // parse JSON body
  .then((user) => {
    console.log(`Loaded ${user.name}`);
    return fetch(`https://api.example.com/users/${user.id}/orders`);
  })
  .then((response) => response.json())
  .then((orders) => console.log(`Found ${orders.length} orders`))
  .catch((error) => console.error("Something failed:", error)); // ANY step failing lands here

Promise utilities you'll use constantly: Promise.all([a, b]) (run independent jobs in parallel, wait for all), Promise.race(...) (first finished wins), Promise.allSettled(...) (wait for all, keep both successes and failures).

Generation 3: Async/await - async code that reads like normal code

async/await (ES2017) is promises in comfortable clothing. Mark a function async, then await each promise inside it. Beginners should learn this style first - it's what modern teams write:

// Real-world example: the SAME user-then-orders flow, rewritten with async/await
// (Same placeholder URL as above - failures land safely in the catch block.)
async function showUserOrders(userId) {
  try {
    const userResponse = await fetch(`https://api.example.com/users/${userId}`);
    const user = await userResponse.json();
    console.log(`Loaded ${user.name}`);

    const ordersResponse = await fetch(
      `https://api.example.com/users/${user.id}/orders`
    );
    const orders = await ordersResponse.json();
    console.log(`Found ${orders.length} orders`);
    return orders;
  } catch (error) {
    console.error("Something failed:", error); // network error, bad JSON, 404…
  }
}

showUserOrders(7);

Read it top to bottom - it looks synchronous, but await politely pauses only this function while the network works; the rest of the page stays responsive. Three rules to memorize:

  1. await only works inside async functions (or at the top level of a module).

  2. async functions always return a promise - even async () => 5 returns Promise<5>.

  3. Wrap await in try/catch - a rejected promise without a catch crashes with an unhandled rejection.

13. The Event Loop: How JavaScript Juggles Without Dropping Anything

JavaScript runs on one thread - one thing at a time. Yet a page can animate, fetch data, and respond to clicks simultaneously. The trick is the event loop, a traffic controller with three parts:

  • Call stack - the "do it now" pile. Functions push on, run, and pop off.

  • Web APIs / Node APIs - helpers outside JavaScript that handle waiting (setTimeout, fetch, file reads). They hold the timer or network request so JS doesn't have to stare at the clock.

  • Task queue - finished callbacks wait here in line. When the stack is empty, the event loop moves the first callback onto the stack to run.

// The classic demo - predict the order BEFORE running it:
console.log("1. First");

setTimeout(() => console.log("2. Timer (0ms!)"), 0);

Promise.resolve().then(() => console.log("3. Promise"));

console.log("4. Last");

// Actual output: 1 → 4 → 3 → 2

Why? console.log runs immediately on the stack (1, then 4). The promise callback jumps a priority lane (the microtask queue - promises always beat timers). The setTimeout(…, 0) callback waits in the regular queue until the stack is fully empty - "0ms" means "as soon as possible," never "right now."

Real-world example - why your loading spinner actually appears:

async function loadDashboard() {
  showSpinner(); // 1. paint "Loading…" - queues a render

  // 2. await YIELDS to the event loop, so the browser can paint the spinner
  //    BEFORE the heavy fetch blocks everything:
  const data = await fetch("/api/dashboard").then((r) => r.json());

  hideSpinner(); // 3. stack is free again - hide spinner, render data
  renderDashboard(data);
}

Without the event loop yielding at await, the spinner would never paint - the page would just freeze until data arrived. Every smooth loading state, progress bar, and autocomplete you've ever used depends on this mechanism. (Rule of consequence: never write long blocking loops on the main thread - chunk the work with await or move it to a Web Worker.)

14. The DOM & Events: Making Pages Interactive

The DOM (Document Object Model) is the browser's live map of the page: every HTML tag becomes a JavaScript object you can find, read, and change. Events (clicks, typing, submits) are how users talk to your code.

<!-- index.html -->
<input id="taskInput" placeholder="New task…" />
<button id="addBtn">Add</button>
<ul id="taskList"></ul>

<script>
  // Real-world example: a working "add task" widget in 10 lines
  const input = document.querySelector("#taskInput");
  const button = document.querySelector("#addBtn");
  const list = document.querySelector("#taskList");

  button.addEventListener("click", () => {
    const text = input.value.trim();
    if (!text) return; // ignore empty submissions

    const item = document.createElement("li"); // build a <li>…
    item.textContent = text;                    // …fill it safely (no HTML injection)…
    list.appendChild(item);                      // …attach it to the page

    input.value = ""; // clear the box for the next task
    input.focus();
  });
</script>

The five DOM moves that build 80% of widgets: querySelector() (find an element), addEventListener() (react to the user), createElement() + appendChild() (add content), textContent (safe text), and classList.add/remove/toggle() (show/hide/style via CSS). Prefer textContent over innerHTML for user-supplied text - innerHTML with untrusted input is an XSS security hole (see Best Practices).

15. JSON & Fetch: Talking to Servers

JSON (JavaScript Object Notation) is the universal language of web APIs - it looks like JavaScript objects, but it's text. fetch() sends HTTP requests and brings back responses.

// Real-world example: submit a contact form to an API
async function submitContactForm(name, email, message) {
  const payload = { name, email, message }; // 1. plain object…

  const response = await fetch("https://api.example.com/contact", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload), // 2. …serialized to a JSON string for travel
  });

  if (!response.ok) {
    throw new Error(`Server said ${response.status}`); // 4xx/5xx → handle it!
  }

  const result = await response.json(); // 3. parse the JSON reply back into an object
  console.log("Ticket created:", result.ticketId);
  return result;
}

// NOTE: api.example.com is a placeholder URL - point it at a real API to run this
// (try https://jsonplaceholder.typicode.com/posts for a free practice API).
submitContactForm("Ana", "[email protected]", "Love your store!").catch((error) =>
  console.error("Request failed:", error.message)
);

Three facts that save beginners hours of debugging:

  1. fetch() only rejects on network failure (offline, DNS). A 404 or 500 is a successful delivery of bad news - you must check response.ok yourself.

  2. JSON.stringify() converts object → text (for sending); response.json() converts text → object (for receiving). Forgetting one direction is the #1 fetch bug.

  3. GET requests (the default) retrieve data; POST creates, PUT/PATCH updates, DELETE deletes. The word for these URL+method combos is a REST API.

16. Error Handling: Expecting the Unexpected

Things fail: networks drop, users type nonsense, APIs change. try/catch/finally lets you fail gracefully instead of crashing:

// Real-world example: parse user settings that might be corrupted
function loadSettings(savedText) {
  try {
    const settings = JSON.parse(savedText); // throws if the text isn't valid JSON
    if (!settings.theme) {
      throw new Error("Settings are missing the theme"); // your own validation!
    }
    return settings;
  } catch (error) {
    console.warn("Using default settings:", error.message);
    return { theme: "light", lang: "en" }; // safe fallback - app keeps working
  } finally {
    console.log("Settings load attempted"); // always runs (cleanup, logging…)
  }
}

console.log(loadSettings('{"theme":"dark"}')); // { theme: "dark" }
console.log(loadSettings("oops-not-json"));    // warns + returns defaults

Guidelines: catch errors where you can do something useful (fallback, retry, friendly message) - don't wrap everything. Let programmer errors (typos, wrong types) crash loudly during development so you actually fix them. And in async functions, try/catch around await replaces .catch() - pick one style per function, not both.

17. Libraries & the Ecosystem: Standing on Shoulders

A library is someone else's tested, reusable code you borrow instead of writing from scratch. The JavaScript ecosystem (via npm, the package registry + installer) is the largest in the world - millions of packages, installed with one command:

npm install date-fns   # a modern date-utility library
import { formatDistanceToNow, subDays } from "date-fns";
console.log(formatDistanceToNow(subDays(new Date(), 3))); // "3 days ago" - human-friendly, zero effort

What beginners should know by category:

  • UI frameworks - React, Vue, Svelte, Angular. For interactive apps with lots of state (dashboards, shops).

  • HTTP clients - fetch (built-in) or axios. For talking to APIs (axios adds timeouts and interceptors).

  • Utilities - lodash, date-fns, zod. For data wrangling, dates, and input validation.

  • Backend - Node.js with Express or Fastify, or Next.js. For servers, APIs, and full-stack apps.

  • Testing - Vitest, Jest, Playwright. For proving your code works (unit tests to end-to-end tests).

  • Tooling - Vite, TypeScript, ESLint, Prettier. For bundling, types, linting, and formatting.

Two honest cautions: (1) learn the language before the framework - React is far easier once sections 1-13 of this post feel natural; (2) every dependency is code you now trust - prefer small, popular, maintained packages, and check their weekly downloads and last-update date before installing.

18. Best Practices: Writing Code Others Love to Read

Best practices aren't fashion - they're bug prevention. These ten pay off from day one:

  1. const by default, let when rebinding, never var. Sealed-by-default variables eliminate an entire class of accidents.

  2. Always ===, never ==. (Unless you can explain type coercion in your sleep - then still use ===.)

  3. Name things like you'd explain them aloud. daysUntilRenewal beats d; isPaid beats flag2. Functions are verbs (calculateTotal, sendEmail); booleans are questions (isValid, hasAccess).

  4. Keep functions small and single-purpose. If a function needs "and" to describe it ("validate and save and email"), split it. Small functions are testable functions.

  5. Don't repeat yourself (DRY) - but don't abstract too early either. Two copies is a coincidence; three copies is a function waiting to be born.

  6. Handle the unhappy path. Check response.ok, validate form input, catch awaits. Assume every external thing fails.

  7. Never trust user input. Use textContent (not innerHTML) for user text; validate on the server too - browser checks are convenience, not security.

  8. Fail fast with clear errors. throw new Error("Order 101 has no shipping address") beats a cryptic crash three functions later.

  9. Let robots handle style. Prettier (formatting) + ESLint (mistake-catching) end all style debates and catch bugs like unused variables automatically.

  10. Comment the why, not the what. // Retry once: the payment API flakes under load helps; // increment i by 1 insults the reader.

19. Beyond This Introduction to JavaScript: Your Practice Roadmap

Reading is 20% of learning; building is 80%. Here's a path in order:

  1. Build tiny DOM projects (this week): a counter, a tip calculator, a todo list, a quiz. Each one exercises variables, functions, arrays, DOM, and events together.

  2. Fetch real data (next): country info from restcountries.com, dummy posts from jsonplaceholder.typicode.com. Practice async/await, loading states, and error handling.

  3. Learn Node.js basics: run your JS outside the browser, read files, build a tiny Express API. Same language, new superpowers.

  4. Add TypeScript: JavaScript + type annotations. It catches at write-time the bugs this post warned about (undefined access, wrong argument types) and is the default in most professional jobs now.

  5. Pick ONE framework (React is the safest career bet) and rebuild a previous project in it. Notice how much is still plain JavaScript underneath.

  6. Write tests with Vitest or Jest. Testing forces small functions and honest error handling - it makes you better at everything above.

Three project ideas that use nearly every section of this post: a shopping cart with promo codes (closures + array methods + this-free design), a weather dashboard with city search (fetch + async + DOM + error states), and a budget tracker with localStorage persistence (JSON + classes + modules).

Cheat Sheet: The Whole Post on One Screen

  • Variables - const default, let to rebind. Key syntax: const total = 0;

  • Types - 7 primitives + objects; null is not undefined. Key syntax: typeof x

  • Comparison - strict only. Key syntax: ===, !==

  • Ternary - one-line if/else. Key syntax: cond ? a : b

  • Functions - reusable recipes; arrows for short callbacks. Key syntax: function f(){}, (x) => x

  • Arrays - transform, don't hand-loop. Key syntax: .map .filter .find .reduce .some .every

  • Scope - inner sees outer, never reverse. Key syntax: global, then function, then block

  • Hoisting - declarations register first; let/const have a TDZ. Key syntax: declare before use

  • Closures - functions remember their birthplace. Key syntax: private state via outer vars

  • this - decided by the call (obj.m() means obj); arrows inherit. Key syntax: prefer arrows in callbacks

  • Prototypes - failed lookups climb the chain. Key syntax: Array.prototype, Object.create

  • Classes - blueprints; sugar over prototypes. Key syntax: class, constructor, extends, super

  • Template literals - strings with ${expressions}. Key syntax: `Hi ${name}`

  • Destructuring - unpack in one line. Key syntax: const { a } = obj;

  • Spread/rest - expand or gather with .... Key syntax: [...a, x], f(...args)

  • Safe access - ?. stops, ?? falls back. Key syntax: user?.address?.city ?? "none"

  • Modules - one concern per file. Key syntax: export / import

  • Async - callbacks, then promises, then async/await. Key syntax: await + try/catch

  • Event loop - stack drains, then queued callbacks run. Key syntax: promises beat timers

  • DOM - find, listen, update. Key syntax: querySelector, addEventListener

  • JSON/fetch - stringify to send, .json() to receive, check .ok. Key syntax: fetch(url, { method: "POST" })

  • Errors - catch where you can recover. Key syntax: try/catch/finally, throw


JavaScript rewards the curious: every strange behavior (typeof null, this changing its mind, 0.1 + 0.2 !== 0.3) has a historical reason, and understanding the why turns gotchas into interview answers. Keep the console open, break things on purpose, and build something small every week. You're further along than you think - now go ship something. 🚀

Featured image: Photo by Rashed Paykary on Pexels.

Related reading

From Zero to Kickass Software Engineer: Using AI as Your Unfair Advantage
career

From Zero to Kickass Software Engineer: Using AI as Your Unfair Advantage

October 8, 2026
ai-assisted-codingbeginnerscareer-changelearning-to-code
scnr: Defensive Vulnerability Discovery That Never Exploits
security

scnr: Defensive Vulnerability Discovery That Never Exploits

October 13, 2026
devopsgolangopen-sourcescnr

Comments

Comments disabled — configure NEXT_PUBLIC_GISCUS_* env vars.