Running Your First Code

Mental model: A statement does something; an expression produces a value. The console is how you watch both.

Level: beginner · about 10 minutes

Start with something that runs. Press Run and read the output before reading the explanation.

const name = 'you';          // a statement: it declares something
console.log('hello, ' + name); // → 'hello, you'
console.log(2 + 2 * 3);        // → 8

Statements versus expressions

Expression: produces a value

2 + 2
'a' + 'b'
[1, 2].length
score > 10 ? 'win' : 'lose'
Math.max(1, 9)

Statement: performs an action

const total = 4;
if (ready) { start(); }
for (const n of nums) { }
return total;
throw new Error('nope');

The test is simple: could you pass it to console.log()? If yes it is an expression. console.log(if (x) {}) is a SyntaxError, because if is a statement. Expressions can sit inside statements; statements cannot sit inside expressions.

let a, b;
console.log((a = b = 5));  // → 5   the assignment evaluates to its value
console.log(a, b);         // → 5 5

const label = true ? 'on' : 'off';  // a ternary is an expression
console.log(label);                 // → 'on'

Assignment is an expression too, which is why this chains.

Semicolons and the rule that bites

JavaScript inserts semicolons for you at line breaks when the next line cannot continue the current statement. That is automatic semicolon insertion, and it is right almost always. Here is one of the places it is not.

function badSum(a, b) {
  return
    a + b;
}

console.log(badSum(2, 3)); // → undefined, not 5

A return on its own line is a complete statement. The value below it is unreachable.

const total = 5
const label = 'total'
[0, 1].forEach((n) => console.log(n))

// Parsed as: const label = 'total'[0, 1].forEach(...)
// → TypeError: "o".forEach is not a function

Do not run this one. Read it, then read the third line as a continuation of the second.

const nums = [1, 2]
const label = 'total'
[0, 1].forEach((n) => console.log(n))

No semicolon is inserted before a line starting with [, so the parser reads 'total'[0, 1]. The comma operator yields 1, and 'total'[1] is the string 'o'. Then .forEach on a string is undefined, so calling it throws a TypeError while running, not while parsing. Either use semicolons or start the line with one.

The console is more than log

CallUse it for
console.log(...)the default. Multiple arguments are fine, no need to concatenate
console.warn / console.errorseverity, and a stack trace attached in most hosts
console.table(rows)arrays of objects, rendered as a grid. Best single upgrade to your debugging
console.group / groupEndcollapsible, indented sections for noisy loops
console.time / timeEndrough duration between two points, keyed by label
console.count(label)"how many times did this run?" without a counter variable
console.trace()"how did I get here?" prints the call path
console.assert(cond, msg)log only when the condition is false
console.dir(obj)the object as a tree, ignoring any custom formatting
const cart = [
  { item: 'keyboard', qty: 1, price: 49 },
  { item: 'cable', qty: 3, price: 4 },
];

console.table(cart);
console.group('totals');
console.log('lines:', cart.length);
console.log('sum:', cart.reduce((t, r) => t + r.qty * r.price, 0)); // → 61
console.groupEnd();

Run this and open the output. table and group render properly in the browser console too.

console.time('work');
for (let i = 0; i < 3; i++) console.count('loop'); // → loop: 1, 2, 3
console.timeEnd('work');                           // → work: 0.1ms

console.assert(1 + 1 === 2, 'arithmetic is broken'); // silent, as it should be

Comments

// a single line

/* a block,
   over several lines */

/**
 * JSDoc: editors read this and show it on hover.
 * @param {number} cents
 * @returns {string}
 */
const money = (cents) => `$${(cents / 100).toFixed(2)}`;
console.log(money(1999)); // → '$19.99'

Script or module?

Classic script

<script src="app.js"></script>

// top-level names can hit the global object
// no import / export
// sloppy mode unless you opt in
// blocks HTML parsing while it downloads

Module

<script type="module" src="app.js"></script>

// import / export available
// strict mode automatically
// its own top-level scope
// deferred by default, top-level await allowed

Write modules. Everything in this course assumes type="module" in the browser and .mjs or "type": "module" in Node. The differences are not stylistic: strict mode and scoping actually change.

AttributeWhen it downloadsWhen it runsOrder preserved?
noneimmediately, parsing stopsstraight awayyes
deferin parallel with parsingafter the HTML is parsedyes
asyncin parallel with parsingthe moment it arrivesno
type="module"in parallel with parsinglike deferyes

Try it yourself

The console family

const rows = [
  { lesson: '1.1', minutes: 8 },
  { lesson: '1.2', minutes: 10 },
];

function report(list) {
  console.group('lesson report');
  console.table(list);
  console.log('total minutes:', list.reduce((t, r) => t + r.minutes, 0));
  console.groupEnd();
}

report(rows);
console.count('report calls');
report(rows);
console.count('report calls');

Swap console.log for console.warn and console.error and compare. Then add a console.trace() inside report to see the call path.

Expression or statement?

console.log(2 ** 10);              // expression → 1024
console.log([1, 2, 3].join('-'));  // expression → '1-2-3'
console.log(((x) => x * 2)(21));   // expression → 42

const state = 'ready';
const message = state === 'ready' ? 'go' : 'wait';  // expression
console.log(message);

// console.log(if (true) { 'nope' });  // SyntaxError: statements have no value

Uncomment the last line. The error you get is the point of the exercise.

Exercises

Fix the ASI bug

sum always returns undefined because a line break ended the return statement early. Fix it so sum([1, 2, 3]) gives 6. Keep the loop.

Build a labelled logger

Write logAll(label, values) which logs one line per value in the form label 1/3: value, using console.log, and returns how many lines it logged. An empty list logs nothing and returns 0.

Check yourself

What does this log?
undefined — Automatic semicolon insertion closes the return at the line break, so the function returns nothing and 'chosen' is dead code. This is why return must never be left dangling at the end of a line.
Which of these is an expression?
ok ? go() : stop() — A ternary evaluates to a value, so you can log it, return it or pass it as an argument. if, const and for are statements: they do something but produce nothing you can hand to a function.
You add defer to a <script> tag. What changes?
The script runs after the HTML is parsed, and script order is kept — defer keeps the download parallel but delays execution until parsing is done, preserving the order of deferred scripts. async also downloads in parallel but runs the instant it arrives, in whatever order the network delivers. type="module" behaves like defer already.

Common mistakes

  • Leaving a value on the line after return, throw, break or continue.
  • Starting a line with ( or [ in a codebase that omits semicolons, and blaming the previous line.
  • Logging bare values with no label, then losing track of which output came from where.

Takeaways

  • Expressions produce values, statements perform actions. "Could you log it?" is the test.
  • ASI usually helps, but never break a line after return, and beware lines that start with ( or [.
  • console.table, group, time and count replace most manual debugging plumbing.
  • Use modules: strict mode, real scoping and deferred execution come for free.