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 5A 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 functionDo 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
| Call | Use it for |
|---|---|
console.log(...) | the default. Multiple arguments are fine, no need to concatenate |
console.warn / console.error | severity, 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 / groupEnd | collapsible, indented sections for noisy loops |
console.time / timeEnd | rough 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 allowedWrite 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.
| Attribute | When it downloads | When it runs | Order preserved? |
|---|---|---|---|
| none | immediately, parsing stops | straight away | yes |
defer | in parallel with parsing | after the HTML is parsed | yes |
async | in parallel with parsing | the moment it arrives | no |
type="module" | in parallel with parsing | like defer | yes |
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
returnat the line break, so the function returns nothing and'chosen'is dead code. This is whyreturnmust 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,constandforare statements: they do something but produce nothing you can hand to a function.- You add
deferto a<script>tag. What changes? - The script runs after the HTML is parsed, and script order is kept —
deferkeeps the download parallel but delays execution until parsing is done, preserving the order of deferred scripts.asyncalso downloads in parallel but runs the instant it arrives, in whatever order the network delivers.type="module"behaves likedeferalready.
Common mistakes
- Leaving a value on the line after
return,throw,breakorcontinue. - 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,timeandcountreplace most manual debugging plumbing.- Use modules: strict mode, real scoping and deferred execution come for free.