● David R. Longnecker
Chapter 2 · Foundations

JavaScript Foundations

JavaScript is the one language that runs unmodified in every browser on earth, and — via a runtime like Node.js — on servers too. Everything you'll build in this course, no matter which framework or tool sits on top, ultimately compiles down to the small set of ideas in this chapter: values, functions, and a peculiar but learnable way of handling things.

Values and variables #

A variable is a name you give to a value so you can use it later. Modern JavaScript gives you two ways to declare one:

let vs const
let count = 0;       // can be reassigned later
count = count + 1;

const title = "Buy milk";  // cannot be reassigned
// title = "Buy eggs";     // this line would throw an error

Default to const. Reach for let only when you know the value needs to change, such as a loop counter or a running total. You will almost never see var in modern code — it's scoped to the nearest function rather than the nearest block, so a var declared inside an if or a for loop "leaks" out and is still visible outside it, which causes bugs let and const simply don't allow.

Every value has a type. JavaScript's primitive types are small and worth memorizing:

TypeExampleNotes
string"todo item"Text, in single, double, or backtick quotes
number42, 3.14No separate integer type
booleantrue, falseTwo values, nothing else
undefinedundefinedA variable that's declared but never assigned
nullnullAn intentional "no value"

The difference between the two "nothing" values shows up constantly in practice. Reading a property that's never been set gives you undefined, silently — no error, no warning:

const todo = { id: 1, title: "Buy milk" };
console.log(todo.dueDate);      // undefined — this property is never set
console.log(todo.dueDate.getMonth());   // TypeError: Cannot read properties of undefined

Nothing stopped you from writing todo.dueDate.getMonth(); JavaScript only complains the moment it actually tries to read a property off of undefined. This exact gap — code that looks fine but crashes on data that happens to be missing — is what strictNullChecks in Chapter 3 catches before you ever run the code.

Both values mean "nothing," so it's fair to ask why JavaScript bothers with two. In practice the distinction is about whose decision the absence is:

One concrete place this shows up: JSON.stringify() drops properties whose value is undefined from the output entirely, but keeps a property whose value is null. If an API needs to tell the difference between "this field is unset" and "this field is explicitly cleared," null is the only one of the two that survives being sent over the network. Many teams settle on a house rule — "we use null for intentional absence, never undefined, in anything we send or store" — specifically to avoid this ambiguity.

Objects and arrays #

Everything more complex than a primitive is built from two shapes. An object is a bag of named fields; an array is an ordered list.

a todo item and a list of them
const todo = {
  id: 1,
  title: "Buy milk",
  done: false,
};

const todos = [
  { id: 1, title: "Buy milk", done: false },
  { id: 2, title: "Write chapter 2", done: true },
];

console.log(todo.title);       // "Buy milk"
console.log(todos[0].title);   // "Buy milk" -- arrays are zero-indexed
console.log(todos.length);     // 2

Arrays come with built-in helpers that avoid hand-written loops for common tasks:

const titles = todos.map(t => t.title);           // ["Buy milk", "Write chapter 2"]
const pending = todos.filter(t => !t.done);        // items where done is false
const allDone = todos.every(t => t.done);          // false
const anyDone = todos.some(t => t.done);           // true

Functions #

A function packages up a piece of logic so you can name it and reuse it. JavaScript has two common syntaxes for writing one:

two ways to write the same function — pick one, not both (declaring addTodo twice in one file is a SyntaxError)
// option A: function declaration
function addTodo(title) {
  return { id: Date.now(), title, done: false };
}

// option B: arrow function assigned to a const — more common in this course
const addTodo = (title) => {
  return { id: Date.now(), title, done: false };
};

// a short arrow function can skip the braces and "return"
const isDone = (t) => t.done;

Date.now() returns the current time as a plain number (milliseconds since a fixed reference point), which is enough to stand in for an id in these early examples — real applications generate ids more carefully, since two calls close together can collide.

Arrow functions are the default style you'll see in this course, especially as short callbacks passed to array helpers like map and filter above. They also don't rebind this, which matters once you start writing component code in later chapters — for now, just treat them as a terser way to write a function.

Strings that mix in variables use a template literal (a backtick-quoted string that embeds expressions with ${...}) rather than concatenation:

template literal, with two interpolated expressions
const name = "David";     // the value being interpolated
const greeting = `Hello, ${name}! You have ${pending.length} todos left.`;

Each ${...} is an interpolated expression: JavaScript evaluates whatever's inside — a variable, a property access, even a function call — and drops the result directly into the string at that position. Above, ${name} and ${pending.length} are each replaced with their current value when the template literal runs.

Tip

Use backticks (`) any time a string needs to include a variable or another expression. It reads more clearly than "Hello, " + name + "!" and it's the style used throughout the rest of this course.

Control flow, briefly #

You've likely seen if/else from other languages, and JavaScript's version is aligned to those "c-like" conventions:

if (pending.length === 0) {
  console.log("All caught up");
} else if (pending.length === 1) {
  console.log("One thing left");
} else {
  console.log(`${pending.length} things left`);
}

for (const t of todos) {
  console.log(t.title);
}

Note ===, not ==. The triple-equals form checks value and type together and avoids a long list of surprising coercion rules that the double-equals form has. Use === and !== by default.

Coming from a strictly-typed language? Read this one

JavaScript will silently convert values of different types to compare them — called type coercion — and the rules are not always intuitive:

0 == false;        // true  -- number coerced to boolean
"" == false;       // true  -- empty string coerced to boolean
"0" == 0;          // true  -- string coerced to number
null == undefined; // true  -- these two are loosely equal to each other...
null === undefined; // false -- ...but never strictly equal
1 === 1;           // true  -- same value, same type, no surprises

=== sidesteps all of this by refusing to coerce at all — if the types differ, the answer is simply false, with no conversion rules to memorize. This matters just as much outside of ==: any if (someValue) check coerces someValue to a boolean, and a handful of values are "falsy" (coerce to false) even though they aren't the boolean false itself: 0, "" (empty string), null, undefined, NaN, and false itself. Every other value — including "0" the string, and an empty array or object — is "truthy." A todo count of 0 being falsy is a classic bug source: if (todo.notes) silently treats an empty string the same as a missing value, which is sometimes what you want and sometimes very much not.

Asynchronous code: juggling code #

Everything above behaves the way you'd expect from any language: one line finishes before the next one starts. Networking breaks that assumption. When your code asks a server for data, the answer might take 5 milliseconds or 5 seconds — and JavaScript refuses to freeze the whole program while it waits.

The event loop, conceptually

JavaScript runs on a single thread with one call stack. When you call a function that has to wait on something slow — a network request, a timer, reading a file — that work is handed off, and the stack keeps running your other code. When the slow thing finishes, its callback is placed on a task queue, and the event loop pulls from that queue only once the call stack is empty.

Synchronous code runs on the call stack; slow work is handed off and its callback waits in a task queue until the stack is empty Call stack fetch(...) loadTodos() main() Network / timers (handled off-thread) Task queue .then(response) callback hands off request runs only once the stack is empty
The call stack keeps running while slow work happens elsewhere; its result waits in a queue until there's nothing left on the stack.

This is why a single-threaded language can still feel responsive: it never blocks on I/O. It just reorders when your callback gets to run.

This diagram simplifies one thing worth knowing about: there are actually two queues, not one. Promise callbacks (the microtask queue) always run before timer callbacks like setTimeout (the macrotask queue), even if the timer was scheduled first. It rarely matters day to day, but it's the answer when two async operations run in a different order than you expected.

Callbacks, then Promises, then async/await

The earliest style for "run this when the slow thing finishes" was a plain callback function:

callback style (you'll rarely write this today)
getTodos(function (todos) {
  render(todos);
});

Nest a few of these and you get "callback hell" — a staircase of indentation that's hard to read and hard to handle errors in. A Promise fixes this by representing "a value that will exist eventually" as an object you can chain:

Promise chain
fetch('/api/todos')
  .then(response => response.json())
  .then(todos => render(todos))
  .catch(error => console.error('Failed to load todos', error));

A promise is always in one of three states: pending, fulfilled, or rejected. .then() runs on fulfillment, .catch() runs on rejection. This is better than nested callbacks, but long chains are still awkward to read compared to ordinary top-to-bottom code.

async/await is syntax sugar over promises that makes asynchronous code read like synchronous code:

async/await — the style you'll use throughout this course
async function loadTodos() {
  try {
    const response = await fetch('/api/todos');
    const todos = await response.json();
    render(todos);
  } catch (error) {
    console.error('Failed to load todos', error);
  }
}

The await keyword pauses execution of this function — and only this function — until the promise settles. The rest of the program keeps running. A function marked async always returns a promise itself, even if you write a plain return inside it.

Key idea

Callbacks, promises, and async/await are three syntaxes for the same underlying mechanism — the event loop and task queue from the diagram above. async/await is just the easiest one to read, so it's what you'll see in every server route and API call for the rest of this course.

Common mistake

Forgetting await doesn't throw an error — it just gives you the Promise object itself instead of the value inside it. If you ever log something and see Promise {<pending>} where you expected data, you forgot an await.

Try it

Open your browser's developer console right here on this page and run fetch('https://drlongnecker.com').then(r => console.log(r.status)). Then try it again with await in front, in a console that supports top-level await. Compare what each line immediately returns versus what eventually logs.

This only works because you're viewing this page on drlongnecker.com itself — a page's JavaScript can only fetch another origin if that server explicitly allows it (CORS, covered properly in Chapter 9). Try the same line from a different site's console and it'll likely fail — a small, concrete preview of a rule you'll see enforced deliberately, on purpose, once you're writing servers of your own.

Organizing code into modules #

Every example so far has lived in one imaginary file. Real programs don't: they're split across many files, each responsible for one thing, that need to share code with each other. A module is just a file that says, explicitly, what it makes available to other files (export) and what it needs from them (import). This is the ES module (ESM) syntax, and it's what every code sample from Chapter 4 onward assumes you already know.

todos.js — a module that exports two things
export function addTodo(title) {
  return { id: Date.now(), title, done: false };
}

export const MAX_TITLE_LENGTH = 200;
main.js — a module that imports them
import { addTodo, MAX_TITLE_LENGTH } from './todos.js';

const todo = addTodo("Buy milk");
console.log(todo, MAX_TITLE_LENGTH);

These are named exports: a file can have as many as it wants, and the importing file picks exactly which names it needs, in curly braces. A file can also have exactly one default export — typically the "main thing" that file is for — imported without curly braces and under whatever name you choose:

a default export
// TodoStore.js
export default class TodoStore { /* ... */ }

// main.js
import TodoStore from './TodoStore.js';   // no braces, and the name is up to you

You'll see both patterns throughout this course: small, focused files with a handful of named exports (a set of route handlers, a set of utility functions), and files with one default export where the file is the thing (a single Vue component, a single class).

Why bother, instead of one big file?

Two reasons. First, every dependency is explicit and traceable — if main.js doesn't import something, it cannot use it, full stop, which makes it possible to reason about one file without holding the entire program in your head. Second, nothing is global by default: two files can each declare their own MAX_TITLE_LENGTH with completely different values and never collide, because each only exists inside the module that declared it unless explicitly exported.

Node.js, modern browsers, and every tool in this course (Vite, Vitest, TypeScript) all understand this import/export syntax natively today. You may still encounter an older syntax, require('./todos.js'), in tutorials, older codebases, or some Node.js internals — it's from CommonJS, an earlier module system that predates ESM. The two are conceptually similar but not directly interchangeable in the same file; this course uses import/export throughout.

Why this matters for everything that follows #

Every framework, build tool, and server library in this course is written in JavaScript or TypeScript (JavaScript plus types — Chapter 3). A component's reactive state is a variable. A click handler is a function. Loading data when a page opens is an async function calling fetch. There is no new execution model waiting for you later — just these same pieces, arranged into increasingly useful structures.