Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 4

Control Flow

This chapter assumes you already know JavaScript's operators, expressions, and variable scoping from earlier chapters, and that you are comfortable reading short functions. Here we treat control flow not as a beginner's "if and while" tour but as a precision tool: what the constructs actually evalua
Level: AdvancedLength: In depth3 min read

Read at another depth

Control Flow

This chapter assumes you already know JavaScript's operators, expressions, and variable scoping from earlier chapters, and that you are comfortable reading short functions. Here we treat control flow not as a beginner's "if and while" tour but as a precision tool: what the constructs actually evaluate to, where they diverge from直觉 intuition, and how their semantics interact with coercion, exceptions, and the event loop.

Branching: `if`, `else`, and the Conditional Operator

if coerces its condition to a boolean using the rules of ToBoolean: 0, NaN, "", null, undefined, and false are falsy; everything else — including [], {}, and -0's sibling objects — is truthy. Two consequences matter at an advanced level:

  • if (value) silently discards the distinction between 0, NaN, "", null, and undefined. When those mean different failure modes, test explicitly: if (value === undefined).
  • ?? (nullish coalescing) and || answer different questions. a || b falls through on any falsy a; a ?? b only on null/undefined. Mixing them without parentheses is a syntax error, which is the language forcing you to be explicit about intent.

The conditional operator cond ? a : b is an expression, not a statement: it evaluates to a value and can appear anywhere an expression is allowed. if/else cannot. That distinction drives style choices in expression-oriented code:

Code example in javascript
const label = count === 0 ? "empty" : count > 100 ? "many" : "some";

// Legal with the conditional operator, illegal with if/else:
const plan = allowed && (premium ? buyPremium : buyStandard);

Chained ternaries read poorly past two levels. Prefer if/else with early returns, or extract a function.

The switch statement uses strict (===) comparison for its cases and, critically, falls through without break. Intentional fallthrough is legal but should be documented with a comment; many linters require it. switch also has a single shared scope for all clauses, so let inside one case leaks to the others — wrap bodies in braces when declaring.

Loops and Their Exit Semantics

JavaScript offers five loop forms, and their differences are semantic, not just syntactic:

ConstructIterates overYieldsWatch out for
`for`anything you control—closure capture of `var`
`for...of`iterables (arrays, strings, Maps)valuesnot usable on plain objects
`for...in`enumerable string keyskeysinherited and non-index keys
`while` / `do...while`condition—`do...while` runs at least once

for...of and for...in are not interchangeable. for...in walks property names, including inherited enumerable ones, and on arrays returns string keys in unspecified relative order for non-index properties. For arrays, for...of (or array methods) is almost always correct:

Code example in javascript
const base = { inherited: true };
const arr = [10, 20, 30];
arr.__proto__ = base;

for (const k in arr) console.log(k);      // "0" "1" "2" "inherited"
for (const v of arr) console.log(v);      // 10 20 30

break exits the nearest enclosing loop or switch; continue skips to the next iteration. Labeled statements let break/continue target an outer loop, which avoids flag variables:

Code example in javascript
outer: for (const row of grid) {
  for (const cell of row) {
    if (cell === target) break outer;
  }
}

Note that for...of in an async function does not implicitly await each element — awaiting inside the loop body pauses per iteration, which serializes work. For concurrency, use Promise.all with map instead (covered in the asynchrony chapters).

Expressions That Control Flow

Several "non-statement" constructs participate in control flow and short-circuit evaluation:

  • && and || return one of their operands, not a boolean: "a" && "b" is "b". Short-circuiting means the right operand is not evaluated at all — the basis of guards like if (obj && obj.ready).
  • Optional chaining a?.b short-circuits the whole chain when a is nullish, yielding undefined without evaluating b.
  • a ?? b short-circuits on nullish a only.

Comma sequencing and assignment-as-expression (a = b = c) are legal but rarely improve clarity at this level.

Exceptions as Control Flow

throw can throw any value, but only Error instances (or subclasses) populate stack and interact well with debugging. try/catch/finally has precise ordering semantics: finally runs whether the block completes, breaks, returns, or throws — and a return inside finally silently replaces any pending return or exception. That is almost always a bug:

Code example in javascript
function risky() {
  try {
    throw new Error("real failure");
  } finally {
    return "ok"; // swallows the error entirely
  }
}

catch without a binding (catch { ... }) is available when the error value is unneeded. Note that catch catches synchronously thrown errors only; errors from async operations require promise rejection handling (later chapters).

The Diagram: One Path Through a `try/finally` Loop

The key insight: finally is on every exit path. Reasoning about loops with cleanup code means enumerating all four exits — completion, break, continue, and throw.

Choosing Constructs: Decision Criteria

  • Guard clauses with early return beat nested if/else for flat, readable logic.
  • for...of over arrays and iterables; never for...in over arrays.
  • ?? and ?. for nullish handling; reserve || for genuine "any falsy default" intent.
  • Avoid return inside finally; let cleanup code throw only if it must.
  • Labeled loops only when nested-loop exit is genuinely two levels deep; otherwise extract a function and return.
Code example in javascript
function process(items) {
  if (!items) return [];
  if (items.length === 0) return [];
  for (const item of items) {
    if (item.invalid) continue;
    if (item.fatal) throw new Error(`bad item: ${item.id}`);
  }
  return items;
}

Exercise

What does the following print, and why? function f() { try { return "try"; } finally { console.log("finally"); } } console.log(f());

Show answer

It prints "finally" then "try". The return value is computed first, then the finally block runs before the function actually returns, so "finally" logs before the caller receives "try".

Exercise

Why does for (const k in [1,2,3]) risk more than just returning string keys?

Show answer

Because for...in enumerates inherited enumerable properties too. If Array.prototype or the array's prototype chain gains an enumerable property, its key appears in the loop, breaking logic that assumes only indices.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Control Flow

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that