Start.dev
Back to blog

Expressions vs statements in JavaScript: what's the difference?

You can read JavaScript but keep forgetting how to write it. The difference between an expression and a statement explains most of the syntax.

Ariel Weinberger
Ariel Weinberger ยท 11 min read

Someone in our Discord shared a line they got from AI Brad, the assistant inside our courses, while they were stuck on a Next.js challenge: "A useful way to remember syntax is to focus on the shape of the data and the job of the syntax, rather than memorizing symbols." Another learner replied that they understood everything they read, but their memory for syntax was shaky.

I remember being in exactly that spot. I could read code, and I still had to look up the same syntax over and over when I wrote it. What eventually fixed it was not more practice. It was understanding the difference between a statement and an expression, well enough that I could look at any line of code and say which one it was. Nobody told me this. I had to notice it on my own, and I wish someone had sat me down and explained it, so that is what this post does.

If you have the same shaky syntax memory, spend an hour on this. It explains most of the rules you are trying to memorize, and at the end there is an exercise so you can check whether it stuck.

What an expression is, and what a statement is

An expression is any piece of code that produces a value. 2 + 3 produces 5. "Ada".length produces 3. A function call produces whatever the function returns. A ternary produces one of its two branches. When AI Brad says "the shape of the data", this is the half it refers to: the first question to ask about any piece of syntax is what value it gives you.

A statement is a piece of code that does something and produces no value. if chooses which branch runs. for repeats. return leaves the function. A block in braces groups other statements. That is "the job of the syntax": statements are about what happens next, not about a value.

The simplest way to check which one you are looking at is to put it inside console.log(...). If it prints, it was an expression.

console.log(2 + 3)                        // 5
console.log("Ada".length)                 // 3
console.log(Math.max(4, 9))               // 9
console.log([1, 2, 3].map((n) => n * 2))  // [ 2, 4, 6 ]

If you try the same thing with a statement, the parser rejects the file before anything runs:

console.log(if (true) { 1 })
// SyntaxError: Unexpected token 'if'

console.log takes arguments, arguments have to be values, and an if has no value to give. That error message is Node's. Bun reports "Unexpected if", which is the same complaint. MDN documents the two halves of the language separately, in the expressions and operators guide and the statements reference, and the Expressions (opens in a new tab) lesson in JavaScript Basics is where the word first comes up in our courses.

Declarations, and the one rule that connects everything

Every piece of code is one of those two kinds, but two details make the picture complete.

The first is that some of what we call statements are, in the grammar, declarations. let, const, function, and class introduce a name, and the language grammar lists them in their own category next to statements rather than among them. For reading code, treating them as statements works: they appear in the same places you find statements at the top level of a file or inside a block, and they produce no value, and that is how I use the word in this post. The one place the shorthand breaks is an unbraced if or loop body, which accepts a statement but not a declaration. The reason to know the grammar's distinction is that when a declared name becomes usable depends on which kind of declaration you used. I come back to this below.

The second is that an expression is allowed to stand on its own line where a statement would normally go. If you write count++ by itself, JavaScript treats it as what the spec calls an expression statement: it evaluates the expression, throws the value away, and moves on. That is how every function call on its own line works.

let count = 0
 
count + 1   // allowed: computes 1, then discards it
count++     // allowed, and the side effect stays
 
console.log(count) // 1

The reverse is not allowed. A statement can never go where a value is expected. This is the one rule I want you to take from this post, because every syntax rule in the next section comes from it: an expression can go anywhere, and a statement can only go where statements go.

Six rules you no longer have to memorize

I used to know each of these as a separate rule. They are all the same rule.

You cannot assign an if, so the ternary exists

if is a statement. It picks a branch to run, but it produces nothing, so there is nothing to assign:

const label = if (score >= 70) { "pass" } else { "retry" }
// SyntaxError: Unexpected token 'if'

When you need a value that depends on a condition, you need an expression, and that is the reason the ternary operator exists:

const score = 72
 
let label
if (score >= 70) {
  label = "pass"
} else {
  label = "retry"
}
 
const label2 = score >= 70 ? "pass" : "retry"
 
console.log(label, label2) // pass pass

?? and && do similar work. nickname ?? "anonymous" produces the fallback when the left side is null or undefined, and score > 90 && "top marks" produces the right side when the left side is truthy and the left side itself otherwise. Because they are expressions, they can go inside arguments, template literals, and JSX. The Ternary (opens in a new tab) lesson covers the details.

An arrow function with braces needs a return

An arrow function whose body is an expression returns that expression. An arrow function whose body is a block contains statements, and statements do not produce a value, so nothing comes back unless you write return:

const doubleA = (n) => n * 2             // expression body
const doubleB = (n) => { n * 2 }         // block body, no return
const doubleC = (n) => { return n * 2 }  // block body with return
 
console.log(doubleA(4)) // 8
console.log(doubleB(4)) // undefined
console.log(doubleC(4)) // 8

The same rule explains a bug almost everyone hits once. () => { name: "Ada" } is not a function that returns an object. The braces are read as a block, name: becomes a label, and the function returns undefined. Wrapping the object in parentheses forces the parser to read it as an expression:

const makeLabel = () => { name: "Ada" }
console.log(makeLabel()) // undefined
 
const makeUser = () => ({ name: "Ada" })
console.log(makeUser()) // { name: "Ada" }

MDN's arrow functions reference documents both body forms, and the Arrow Functions (opens in a new tab) lesson has you write both.

Function declarations are available early, function expressions are not

function square(n) { ... } on its own line is a declaration. JavaScript registers it before it runs the file, which is why you can call it above the line that defines it. const double = function (n) { ... } is different. It declares double, and the function on the right side is an expression, a value like any other, so it only exists once that line has run:

console.log(square(4)) // 16
 
function square(n) {
  return n * n
}
 
console.log(double(4))
// ReferenceError: Cannot access 'double' before initialization
 
const double = function (n) {
  return n * 2
}

MDN's hoisting entry explains the mechanics. The thing to notice is that the difference has nothing to do with the keyword function. It comes from whether the function is a declaration or an expression.

Assignment produces a value

a = 5 is an expression that produces 5. That is why assignments can be chained and why they can appear inside larger expressions. attempts++ produces the old value and then increments, and ++attempts increments first:

let a
let b
 
console.log((a = 5)) // 5
b = a = 10
console.log(a, b)    // 10 10
 
let attempts = 0
console.log(attempts++) // 0
console.log(++attempts) // 2

This is a good place to separate two ideas that often get mixed up. Whether code produces a value and whether it changes something are independent. Assignment does both. A pure function does the first without the second, and keeping the two apart on purpose is what the Pure Functions And Side Effects (opens in a new tab) lesson in Software Design Principles is about.

await produces a value

await is an operator. It pauses the async function until the promise settles. If the promise is fulfilled, await produces its value, so it can sit inside arithmetic, template literals, and function arguments. If the promise is rejected, await throws the rejection reason instead, and the surrounding expression never completes unless something catches the error.

const total = (await fetchPrice()) * 2
console.log(total) // 40
 
const label = `Price: ${await fetchPrice()}`
console.log(label) // Price: 20

The parentheses in the first line are optional, because await binds more tightly than *, but I find they make the order easier to read.

JSX only accepts expressions inside braces

In React, the curly braces in JSX hold expressions and nothing else. The React docs on JavaScript in JSX with curly braces say so directly, and it is the reason conditional rendering uses && and the ternary instead of if, and lists use .map() instead of for:

<p>{isMember ? "Member" : "Guest"}</p>   // an expression, allowed
<p>{if (isMember) { "Member" }}</p>      // a statement, syntax error

You do not need to memorize "no if in JSX". It is the same rule again, and the Expressions In Jsx (opens in a new tab) and Conditional Rendering (opens in a new tab) lessons in React Fundamentals teach it that way.

Try it on ten examples

Here are ten pieces of ordinary code. Classify each one as written: is it an expression or a statement? If it is a statement, say whether it is a declaration, and if it contains an expression, say which part that is. Some of the expressions could also stand on their own line in a program, where they would become expression statements. That is the bridge from earlier, not a different answer. Write your answers down before you look at the table.

  1. price * quantity
  2. const total = price * quantity
  3. if (isMember) discount = 0.1
  4. isMember ? 0.1 : 0
  5. user.save()
  6. function greet() {}
  7. const greet = function () {}
  8. items.map((item) => item.price)
  9. for (const item of items) total += item.price
  10. return total
LineKindWhy
1ExpressionIt multiplies two values and produces one.
2Statement, a declarationIt binds total. The initializer, price * quantity, is the expression.
3Statementif chooses. The discount = 0.1 inside it is an expression used as a statement.
4ExpressionA ternary produces one of two values.
5ExpressionA call produces a value even when you ignore it. On its own line it is an expression statement.
6Statement, a declarationA function declaration, registered before the code runs.
7Statement, a declarationIt binds greet. The function on the right is an expression.
8ExpressionThe call produces an array, and the arrow function inside it is an expression too.
9StatementA for...of loop. total += item.price is the expression it runs each time.
10Statementreturn is a statement. total is the expression it hands back.

If you got most of them, you have what you need, and the rest of the post is about putting it to use. If several were hard, reread the section on the one rule and try again tomorrow. It usually takes a couple of passes.

If you want to practice this with guidance, JavaScript Basics introduces the vocabulary, Intermediate JavaScript uses it constantly in array methods and async code, TypeScript Fundamentals is about the shape of the value every expression produces, and Software Design Principles turns the difference between producing a value and causing a side effect into a design habit:

How this changes the way you write code

What changed for me was the question I ask before I type. I used to ask what the syntax for something was. Now I ask what the surrounding code needs from me: a value, or an action. And if it is a value, what shape the value needs to have.

  • If I need a value that depends on a condition, I need an expression, so I reach for a ternary rather than an if.
  • If I need to run several steps and maybe stop early, that is statement work, so I write a block with if and return.
  • If I need to hand a function to something else, I need the function as a value, so I write a function expression or an arrow function, not a declaration.
  • If I need the result of a promise inside a bigger calculation, await produces a value, so it goes right where the value is needed.

Once you ask those questions, the symbols follow. That is what AI Brad's advice means in practice: the shape of the data and the job of the syntax are what you need to understand, and the punctuation comes out of that understanding.

A concrete way to practice: open any file you wrote recently, pick twenty lines, and classify each one out loud. We are also adding on-demand quizzes to the Study Advisor. Until those ship, you can ask it to give you code snippets to classify, and it will tell you whether you got each one right.

For what it is worth, JavaScript's choice here is not universal. Rust, Kotlin, and Ruby treat if as an expression, and there is a TC39 proposal that would let JavaScript do something similar. Until that changes, the rule is the one above, and you can work it out instead of memorizing it.

The habit that makes it stick

Before you type, ask what the surrounding code needs from you. If it needs a value, write an expression. Expressions go wherever the grammar expects a value: a function argument, the right side of an assignment, the braces in JSX. Most of them can also stand on their own as expression statements, with one exception worth knowing: an expression statement cannot start with {, function, or class, because the parser would read those as a block or a declaration, which is why the object literal earlier needed parentheses. If it needs something to happen, write a statement. Statements only go where the grammar expects a statement: the top level of a file, the inside of a block, or the body of an if or a loop, and never where a value is expected. Ternaries, arrow bodies, hoisting, and JSX are all that same question with a different answer.

If you want to see the same way of thinking applied to a harder topic, our closures guide uses it to work out what a function remembers.

Level up your coding skills

Jump into interactive courses and start building real-world projects.

Start learning

Related posts

Learn JavaScript closures with tested examples: counters, the var vs let loop question, private state, debounce, and the stale closure bug in React hooks.
Brad Traversy
Brad Traversy ยท 8 min read
Learn the JavaScript array methods you will use daily: map, filter, find, some, every, reduce and sort, with runnable examples on one shopping cart.
Ariel Weinberger
Ariel Weinberger ยท 5 min read
You copied an array, changed the copy, and the original changed too. Here is what a JavaScript variable actually holds and how to copy safely.
Ariel Weinberger
Ariel Weinberger ยท 6 min read