JavaScript Statements: Unlocking the Power of Code Control

Hello! I'm a dedicated software developer with a passion for coding and a belief in technology's impact on our world. My programming journey started a few years ago and has reshaped my career and mindset. I love tackling complex problems and creating efficient code. My skills cover various languages and technologies like JavaScript, Angular, ReactJS, NodeJs, and Go Lang. I stay updated on industry trends and enjoy learning new tools. Outside of coding, I cherish small routines that enhance my workday, like sipping tea, which fuels my creativity and concentration. Whether debugging or brainstorming, it helps me focus. When I'm not coding, I engage with fellow developers. I value teamwork and enjoy mentoring newcomers and sharing my knowledge to help them grow. Additionally, I explore the blend of technology and creativity through projects that incorporate art and data visualization. This keeps my perspective fresh and my passion alive. I'm always seeking new challenges, from open-source contributions to hackathons and exploring AI. Software development is more than a job for me—it's a passion that drives continuous learning and innovation.
Last updated: September 2026 — added for...in, labeled statements, throw and error types, optional catch binding; fixed corrupted code blocks.
JavaScript statements are the instructions that make up a program. Expressions produce values; statements make things happen — they declare variables, choose between paths, repeat work, and jump out of it.
This article covers every statement type in the language, what each one is for, and the ones that catch people out.
Expression Statement: The Building Blocks of JavaScript
An expression statement is an expression used on its own as a statement. It performs an action or calculation, and the resulting value is either assigned somewhere or discarded.
let greeting = "Hello, World!";
Here we declare a variable named greeting and assign it the string "Hello, World!". Function calls, assignments and increments are all expression statements:
count++;
doSomething();
user.name = "Ganesh";
Compound and Empty Statements: The Pioneers of Code Composition
Two statement types shape the structure of your code rather than doing work themselves.
Compound Statements: The Art of Encapsulation
A compound statement — also called a block — is a group of statements enclosed in curly braces {}. It lets you use several statements anywhere the syntax allows one, which is what makes functions, loops and conditionals possible.
if (condition) {
// This is a compound statement
console.log("Condition is true.");
// Additional statements go here
}
Blocks also create scope for let and const — a variable declared inside a block does not exist outside it. var ignores blocks entirely, which is one of the reasons to avoid it.
Empty Statements: The Puzzling Semicolon
The empty statement is a single semicolon ; and does nothing. It exists because the grammar sometimes requires a statement where you have no work to do.
It shows up when a loop's entire job is done in its header:
let sum = 0;
const numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; sum += numbers[i++]); // <-- the ; IS the body
console.log(sum); // 6
That trailing semicolon is the loop body. Write it on its own line, or add a comment, because an accidental one is a nasty bug:
for (let i = 0; i < 5; i++); // oops — the loop body is empty
console.log(i); // runs once, and i is not defined here
Most style guides ban the empty statement for exactly this reason.
Conditional Statements: Navigating the Path of Decision-Making
Conditional statements let code choose between paths. JavaScript gives you four ways to do it.
1. if Statement: The Gatekeeper of Conditions
The if statement evaluates a condition and runs a block if it is truthy.
if (age >= 18) {
console.log("You are eligible to vote.");
} else {
console.log("You are not eligible to vote.");
}
The condition does not have to be a boolean. Any value works, and JavaScript converts it — which means only the eight falsy values (false, 0, -0, 0n, "", null, undefined, NaN) take the else branch. Everything else, including an empty array and an empty object, is truthy.
2. else Statement: The Alternating Path
The else clause offers an alternative route when the condition is false. It has no condition of its own — it catches everything the if did not.
3. else if Statement: The Multi-Pronged Decision-Maker
When multiple conditions must be assessed, else if evaluates them in order and stops at the first match.
if (score >= 90) {
console.log("You got an A.");
} else if (score >= 80) {
console.log("You got a B.");
} else {
console.log("You got a C or below.");
}
Order matters. If the >= 80 check came first, a score of 95 would report a B — the chain stops at the first true condition, not the best one.
4. switch Statement: Simplifying Complex Choices
switch selects one of many blocks based on the value of an expression.
let day = "Monday";
switch (day) {
case "Monday":
console.log("It's the start of the week.");
break;
case "Friday":
console.log("Weekend is approaching.");
break;
default:
console.log("It's a regular day.");
}
Two things to know about switch.
It compares with ===. No type conversion happens, so a string "1" will not match case 1:
switch ("1") {
case 1: console.log("number 1"); break;
default: console.log("no match — switch uses ===");
}
// logs: no match — switch uses ===
Cases fall through without break. A forgotten break is one of the classic JavaScript bugs — but it is also useful on purpose, for grouping cases:
function describe(n) {
switch (n) {
case 1:
case 2:
return "one or two"; // both 1 and 2 land here
case 3:
return "three";
default:
return "other";
}
}
describe(1); // "one or two"
describe(2); // "one or two"
A return ends the function, so it works the same way break does here.
Loops: The Art of Repetition and Iteration
Loops repeat actions and iterate over data. JavaScript has five.
1. for Loop: The Workhorse of Iteration
The for loop runs a block a specified number of times, controlled by its three-part header.
for (let i = 0; i < 5; i++) {
console.log("Iteration " + (i + 1));
}
2. for/of Loop
The for...of loop iterates over the values of an iterable — arrays, strings, Map, Set, and anything implementing the iterable protocol.
Using for...of with Arrays:
const fruits = ['apple', 'banana', 'cherry'];
for (const fruit of fruits) {
console.log(fruit);
}
Each iteration assigns the current element to fruit.
Using for...of with Objects:
Plain object literals are not iterable, so for...of does not work on them directly. Map and Set are iterable, and so are the results of Object.keys(), Object.values() and Object.entries().
Using for...of with Maps:
const myMap = new Map([
['name', 'John'],
['age', 30],
['city', 'New York']
]);
for (const [key, value] of myMap) {
console.log(`${key}: ${value}`);
}
Each entry is a [key, value] array, destructured in the loop header.
For a plain object, use Object.entries():
const user = { name: "Ganesh", role: "dev" };
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
3. for/in Loop: The One to Be Careful With
The for...in loop iterates over the keys of an object. It looks almost identical to for...of and behaves very differently.
const user = { name: "Ganesh", role: "dev" };
for (const key in user) {
console.log(key, "=", user[key]);
}
// name = Ganesh
// role = dev
It works on arrays too, and that is where the trouble starts. The indexes come back as strings:
const arr = ["a", "b", "c"];
for (const i in arr) {
console.log(i, typeof i); // "0" string, "1" string, "2" string
console.log(i + 1); // "01", "11", "21" <-- concatenation, not addition
}
There is a second problem: for...in walks the prototype chain, so it picks up inherited properties that were never on your object.
const base = { inherited: true };
const obj = Object.create(base);
obj.own = 1;
for (const key in obj) console.log(key); // "own", then "inherited"
The rule:
Objects →
for...of Object.entries(obj), orfor...inwith anObject.hasOwn(obj, key)guard.Arrays →
for...of,.forEach(), or.entries()when you need a real numeric index:
for (const [i, value] of arr.entries()) {
console.log(i, value); // 0 'a' — i is a number
}
4. while Loop: The Sentinel of Conditional Repetition
The while loop continues as long as its condition remains truthy.
let count = 0;
while (count < 5) {
console.log("Count: " + count);
count++;
}
5. do...while Loop: The Guarantor of At-Least-Once Execution
The do...while loop checks its condition after running the body, so the body always executes at least once.
let number;
do {
number = Number(prompt("Enter a positive number: "));
} while (Number.isNaN(number) || number <= 0);
Note the Number() conversion. prompt() returns a string, and comparing a string with <= triggers a type conversion that works by accident more often than by design.
Jumps: Controlling Code Flow with Precision
Jump statements move execution somewhere other than the next line.
1. break Statement: The Escape Artist
break exits a loop or a switch immediately.
for (let i = 0; i < 10; i++) {
if (i === 5) {
break;
}
console.log("Iteration " + i);
}
2. continue Statement: The Skipper of Iterations
continue skips the rest of the current iteration and moves to the next.
for (let i = 0; i < 5; i++) {
if (i === 2) {
continue;
}
console.log("Iteration " + i);
}
3. Labeled Statements: Breaking Out of Nested Loops
break and continue only affect the innermost loop. A label lets you name an outer loop and jump out of that one directly.
outer:
for (const row of [[1, 2], [3, 4]]) {
for (const cell of row) {
if (cell === 3) break outer; // leaves BOTH loops
console.log(cell); // 1, 2
}
}
Without the label you would need a flag variable checked after the inner loop — more code, and easier to get wrong. continue label works the same way, skipping to the next iteration of the labeled loop.
Use this sparingly. If you reach for it often, the loop body probably wants to be a function with an early return.
4. return Statement: The Exit Door of Functions
return exits a function and hands a value back to the caller.
function add(a, b) {
return a + b;
}
A function with no return, or a bare return;, gives back undefined. And return outside a function is a SyntaxError — it belongs to functions only.
5. throw Statement: Raising an Error
throw stops normal execution and hands an error to the nearest enclosing catch.
function withdraw(balance, amount) {
if (amount > balance) {
throw new Error("Insufficient funds");
}
return balance - amount;
}
You can throw any value, but throwing anything other than an Error loses the stack trace:
throw "Something went wrong"; // works, but no stack — hard to debug
throw new Error("Something went wrong"); // do this
Miscellaneous Statements: Navigating the Quirks of JavaScript
1. try...catch...finally: Taming Errors with Grace
The try, catch and finally blocks handle exceptions that occur while code runs.
The try block wraps code that might throw. If an error occurs inside it, control transfers to the catch block.
The catch block receives the error object and decides what to do with it. If no error was thrown, it is skipped entirely.
The finally block is optional and runs either way — error or no error. It is for cleanup that must happen regardless: closing a file, releasing a lock, hiding a loading spinner.
try {
// Code that may throw an exception
} catch (error) {
// Code to handle the exception
} finally {
// Code that always executes, whether there was an error or not
}
Optional catch binding
If you do not need the error object, ES2019 lets you leave the parameter out entirely:
try {
JSON.parse(input);
} catch {
return null;
}
Before this, catch (e) was required even when e was never used — an unused variable in every linted codebase.
Use it only when you genuinely do not need the error. Swallowing errors silently is how a bug becomes invisible.
The built-in error types
Knowing which error type you are looking at narrows the cause before you read the message:
| Type | What it means |
|---|---|
TypeError |
A value is the wrong type — calling a non-function, reading a property of undefined |
ReferenceError |
A variable does not exist, or is in the temporal dead zone |
SyntaxError |
Code could not be parsed — often from JSON.parse |
RangeError |
A number outside its allowed range — new Array(-1), infinite recursion |
Error |
The generic base, and what you extend for your own |
You can subclass Error for your own domain errors, so callers branch on the type rather than matching message strings:
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
try {
throw new ValidationError("email", "Email is not valid");
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.name, err.field, err.message);
// ValidationError email Email is not valid
}
}
Setting this.name matters — without it the error prints as Error and the type is invisible in your logs.
There is also Error.cause (ES2022), for wrapping a low-level error in a meaningful one without losing the original:
try {
JSON.parse(rawConfig);
} catch (err) {
throw new Error("Could not load user config", { cause: err });
}
2. with Statement: Best Left Alone
The with statement was meant to let you access an object's properties without repeating its name.
with (object) {
// Code that refers to object's properties and methods
}
Do not use it. It makes it impossible for a reader — or the engine — to tell whether a name refers to a property of the object or a variable from an outer scope, which blocks optimisation and produces genuinely confusing bugs.
It is a SyntaxError in strict mode, not merely discouraged. Since ES modules and class bodies are always strict, with cannot appear in most modern code at all. It is here so you recognise it in old code, nothing more.
3. debugger Statement
The debugger statement acts as a breakpoint written into your code. When devtools are open, execution pauses there and you can inspect variables, objects and the call stack.
// Your JavaScript code here
// ...
debugger; // execution pauses here when devtools are open
// ...
// More code here
With devtools closed it does nothing. Still, remove them before shipping — a stray debugger will freeze the page for anyone who happens to have devtools open.
4. "use strict" Statement
The "use strict" directive enables a stricter set of language rules. It catches common mistakes that otherwise fail silently:
"use strict";
x = 10; // ReferenceError: x is not defined
Without strict mode, x would quietly become a global variable.
Strict mode also makes this undefined in a plain function call rather than the global object, turns duplicate parameter names into a syntax error, and makes writing to a frozen or read-only property throw instead of failing silently.
You are probably already in strict mode. ES modules and class bodies are strict automatically, with no way to opt out. If you are writing import/export, or working in almost any modern build setup, the directive is redundant. It still matters in plain <script> tags without type="module", and in CommonJS files in Node.
Declaration Statements: Laying the Foundation
Declaration statements create variables, functions and classes.
1. Variable Declarations (var, let, const)
var is function-scoped. It ignores blocks entirely:
function demo() {
if (true) {
var y = 2; // function-scoped — still visible below
let z = 3; // block-scoped — gone at the closing brace
}
console.log(y); // 2
console.log(z); // ReferenceError: z is not defined
}
A var at the top level of a script does become a global, which is where the "var is global" idea comes from — but inside a function it is not.
let and const (ES6) are block-scoped and better behaved:
let age = 30;
const PI = 3.14;
All three are hoisted, but differently. var is hoisted and initialised to undefined, so reading it early is legal and useless. let and const are hoisted but not initialised — the gap before the declaration line is the temporal dead zone, and touching them there throws:
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 1;
Read that error carefully — "cannot access before initialization", not "is not defined". JavaScript knows b exists and is refusing to let you read it yet, which is exactly the bug you wanted caught.
One more thing about const: it prevents rebinding, not mutation.
const user = { name: "Ganesh" };
user.name = "Jaiwal"; // allowed — changing what is inside
user = { name: "Someone" }; // TypeError: Assignment to constant variable
Default to const, use let when you actually reassign, and avoid var.
2. Function Declarations
Function declarations define named functions, hoisted to the top of their scope — which means you can call one before the line that defines it.
greet("Ganesh"); // works, thanks to hoisting
function greet(name) {
return "Hello, " + name + "!";
}
Function expressions assigned to let or const are not usable early, because the variable is in the temporal dead zone.
3. Class Declarations
Class declarations (ES6) define classes. Unlike function declarations, they are not usable before their definition — classes sit in the temporal dead zone the same way let does.
class Person {
constructor(name) {
this.name = name;
}
}
Statements are how a program moves: choosing a path, repeating work, jumping out of it, and recovering when something breaks. Expressions produce the values; statements decide what happens to them.
Next in the series: Mastering JavaScript Objects.
Related reading: Why Should We Use for...of Instead of forEach?
Hope you like it, if yes ❤️ like & 📤share.
Thanks for your time.
Happy Coding...





