88 comments

[ 2.8 ms ] story [ 151 ms ] thread
No intent to sound negative, but doesn’t explanation by numerous cues mud the water even more? I bet most of students are left confused with this “wrapper statement” thing. Magical thinking induced.

If the question is how they are different, then the answer is grammatically, and then the explanation of a grammar follows.

If it is why we make such distinction, then the answer is because the language creator decided so, followed by tradeoffs and pitfalls of making a specific language construct to have a value.

I feel the same towards the article, I think that the moment you need to resort to "think of it as..." it's the moment you lose me.

I also don't think he does a good job at explaining the biggest source of misunderstanding. Quoting the article:

`Expressions and statements are distinct things.`

That is not really correct.

A function call, e.g. is both an expression and a statement:

`foo(bar)` on it's own is both an expression (it returns a value, even if the value returned is undefined) and a statement.

In fact, determining if a piece of code is an expression or a statement in all borderline cases depends on the context.

```

const foo = function bar(){};

function bar(){}

```

On line 1 the bar function is an expression, on bar 3, is a statement.

There's nothing really about the function on it's own that can tell you whether it is an expression or a statement.

On the other hand, a nice way to differentiate statements from expressions is that you cannot use statements where you need expressions.

```

console.log(if (3 > 2) {})

const foo = while (i < 3) {} ```

both lines are syntax errors.

I've never read the ECMAScript spec; but searching through it, I can see that the terms they use is function declaration vs function expression. According to their terminology, function bar(){} on the second line of your function example is a function declaration.
Yes, and a function declaration is a statement.
The way I think of it is that statements involve keywords from the language (like 'let', 'export class', or 'if') while expressions do not.

With this definition, it's easy to see that, if the syntax starts with a language keyword, it's a statement.

BTW, language functions (like 'Array.join()') are expressions because they don't start with a keyword.

But that logic falls apart with languages such as Rust, where a lot of expressions start with a keyword.
Ah. Was just thinking of JS/TS in this case. Good point.
Something which was once a statement and started with a keyword can become an expression in a new version of a language.

For example, in C# 7, throws were made into expressions to allow syntax like this:

  return a ? b : throw new Exception();
So I wouldn't rely on this rule.
Ouch! I like to rely on the ternary operator as a cheat for if/then just as much as the next guy, but that qualifies as abuse, lol!
I wouldn't say that's abuse and I missed that many many times in JS/TS, for example on unreachable variants in tagged enums. This usually turns into null with a null check afterwards just to throw the exception, which is more verbose for no reason.
That doesn't really work, since `alert("Hello, World!")` or `doSomething()` are usually statements, not expressions; while `new SomeFunction()` is usually an expression, as is `'abc' in ['abc']`

I think that, in the context of JavaScript, if you want a "quick and dirty" way to differentiate them, the `console.log( /* put thing here */ ) ` idea from the article is the best way to go - if you can validly put the thing there, it's an expression, otherwise it's a statement.

`alert("hello")` is an expression and also a statement.

  a = alert("hello"); // Assigns a to undefined
What isn't an expression? An `if` statement is not an expression.

  a = if (true) alert("hello"); // Uncaught SyntaxError: Unexpected token 'if'
My point was that GP's rule of thumb (if it has a keyword, it's a statement, otherwise is an expression) is wrong on both ends.

A JS program is composed out of statements, which may contain expressions. In particular, in JS, any expression is also valid as a statement. This is not true in all languages (for example, `2` is not a valid statement in Go).

> My point was that GP's rule of thumb (if it has a keyword, it's a statement, otherwise is an expression) is wrong on both ends.

I do agree!

Sorry to bother you but I am a bit confused about this part of your comment:

> `alert("Hello, World!")` or `doSomething()` are usually statements, not expressions;

Can you please clarify what you mean here? Do you mean `alert("Hello, World!")` is an expression? Or do you mean it is not an expression? Or did you mistype?

Your `console.log( /* put thing here */ )` test is a pretty good one and indeed `console.log(alert("Hello, World!"))` passes this test.

I meant that you would usually encounter those as statements, as in the following example:

  function foo() {
    alert("Hello, World!");
    doSomething();
  }
And not as expressions (as in your example of `console.log(alert("Hello, World"))`.

Btw, a better rule of thumb for JS is "if you can add `;` after it and not alter the meaning of the program, it's a statement; otherwise it's an expression".

There is some historical precedent for what you said. The first programming languages were assembly languages, which only have statements that you write one per line. You can sort of say that each instruction name is a "keyword".

    mov eax ebx
    jump 1235
    xor eax eax
The first big programming language with expressions was Fortran. Its expressions were for mathematical formulas. Everything else was a statement that you still had to write one per line (and in under 80 columns, to fit in a punch card).

Part of the reason fortran was like this is that parsing algorithms were quite primitive. Ideas such as lexing, "tokens", and context-freem grammars, which are standard today, weren't common back then. For example both of the following were valid ways to start a loop in Fortran. Note the lack of whitespace in the second!

    do i = 1, 10
    doi = 1,10
Moving on, as compiler writers got better at parsing it became more popular to allow free formatting in programming languages. Instead of mandating one statement per line, use semicolons as statement terminators and let you format it as you see fit. Over time this led to a blurrying of the distinction between statement and expression. For example, in C and Javascript many things that used to be statements are now expressions (one notable example being assignments). This is taken the furthest in functional programming languages, where almost everything is an expression.
Yes! Maybe I think of it this way because I'm over 50. I originally worked on System/370 assembler and Fortran at IBM.
JavaScript have operators which are keywords, e.g. instanceof and void. "void(2 + 2)" is a valid expression
If this compiles, <something> is an expression:

    let x = <something>;
If this compiles, <something> is a statement:

    <something>;
Both, either, or neither can be true.
So 'if', 'for', 'while' are not statements?
No. Those are syntactic elements.
Not on their own. A full if, for or while (with the conditions and body) is usally a statement, and might be an expression in some languages.
The body is also not terminated by a semicolon.

`while (true) { puts("Hello World\n"); };`

What you've written there does compile though, which was the original condition :).
I get the joke, but it's two statements, not one. (while and ;)
In JavaScript no. But in some languages it can be.
I can't think of an example where

    let x = <something>;
compiles but

    <something>;
does not compile. I think all expressions are statements.
Some languages forbid "obviously pure" expressions like "0" as statements (since using them as a statement is probably a mistake).
Interesting! Any examples of such languages?
Java is one popular example. You get "error: not a statement".
I think I was saying that Javascript is not one of them.
(comment deleted)
(comment deleted)
(comment deleted)
Right. They're not mutually exclusive -- hence the "both, either, or neither"
My point was that there are four combinations, and "both, either, or neither" suggests that all four can happen. But actually, only three out of four can happen.

It can be both a statement and an expression. Yes.

It can be neither a statement nor an expression. Yes.

It can be a statement but not an expression. Yes.

But the case that it is an expression but not a statement: this case never happens.

Statements are orders, expressions are questions.
The answer to a question to be precise.
> The answer to a question to be precise.

No. The expression before evaluation (i.e. as it's written) is a question/query/ask, after a successful evaluation you get a value - an answer. In case when the evaluation failed, you get either a compilation/parsing error, runtime error/exception, crash, infinite loop/no return, or undefined behavior.

--

- Commands and Queries (as in CQS[1] and CQRS[2])

- Wishes and Asks (it's a wish b/c unlike command it can be rejected, and for the same reason it's an ask and not a query - i.e. "you may ask, but it doesn't mean I'll answer").

- Truth or dare?[3] (Dares and Truth-es ;)

--

[1] https://en.wikipedia.org/wiki/Command%E2%80%93query_separati...

[2] https://martinfowler.com/bliki/CQRS.html

[3] https://en.wikipedia.org/wiki/Truth_or_dare%3F

Ask yourself why Lisp has no statements. Is it impossible to issue orders to the Lisp runtime?
Lisp does have statements, such as `setf` or `loop`. Just because you also attach an expression to it because hey, gotta evaluate to something, doesn't mean it's not fundamentally a statement.

Sure, syntactically it's an expression-based language, but some of those are expressions in syntax only. If a function terminates in nil, what exactly did it compute? And what exactly is the point of all but the last argument to a `progn`?

An expression that is evaluated for the side effects, not the result value, is a statement in every way that matters.

(Haskell has a stronger claim because it actually captures the side effects as a value.)

> Lisp does have statements, such as `setf` or `loop`. Just because you also attach an expression to it because hey, gotta evaluate to something, doesn't mean it's not fundamentally a statement.

I think the general consensus is that expressions evaluate to values in unexceptional situations, full stop. Whether they have side effects is immaterial. Whether the value returned is useful or not is also immaterial. Perhaps your examples weren't the best, since both setf and loop evaluate to values which are often useful.

This isn't just Lisp. For example, the C99 specification explicitly declares that all function calls are expressions, even if they return void.

Wikipedia also defines an expression (vs. statement) unambiguously as "a syntactic entity in a programming language that may be evaluated to determine its value.... Expression is often contrasted with statement—a syntactic entity that has no value (an instruction)."

https://en.wikipedia.org/wiki/Expression_(computer_science)

I think you're mixing up syntax and semantics. Function calls that return void are usually better classified semantically as procedures, at least when trying to understand how the program is arranged.
> If a function terminates in nil, what exactly did it compute?

nil.

The fact that you can syntactically put loops and ifs in place of expressions is what makes them expressions

Lotta CPU time spent to get you that nil.

One would think you could just `(defun fun () nil)`.

There seems to be a disagreement about what an expression is.

I have the impression that you think that an expression is a way to compute a value, while I would posit that an expressions are a method of combining values.

Not really an expression:

    a0 = 1;
    a1 = f(2);
    if (a0 < a1){
      a2 = getInput();
    } else {
      a2 = 3;
    }
    result = a0 + a1 + a2;

More of an expression:

    result = 1 + f(2) + (a0 < a1 ? getInput() : 3);
As a programming teacher I think this is quite a fundamental thing to learn for new programmers. Even though lots of tutorials and books don't mention this explicitly people usually get an intuition for the difference after a while.

But in the beginning it's hard for people to grok an expression with a couple of nested expressions in it. Especially if there's some functions calls in there as well.

Axel Rauschmayer has a good page (1) on this topic as well. Quite dry, so less beginner friendly.

So what I like about Josh W Comeau's article is that it's easy to read with lots of examples and even an interactive widget.

Oh and about interactive visualization of expression evaluation: Thonny (2) does this real nicely for Python code. Excellent for beginners imo.

[1] https://2ality.com/2012/09/expressions-vs-statements.html

[2] https://thonny.org/

The article left me more confused than enlightened.

My understanding is this; Expressions return a value, statements on the other hand are actions, commands, definitions, declarations, assignments, and control structures.

Not a JS-specific answer to this question:

expression

- generally returns a value, possibly a void value, or doesn't return control at all.

- it consists out of operators, operands, and function calls

- can be composed out of one or more sub-expressions, and the order of their evaluation isn't always defined (i.e. to allow compiler optimizations).

- usually it doesn't have side-effects (i.e. pure), unless it uses random, time, IPC, system, or HW-specific pseudo-functions, pseudo-variables, or macros as a sub-expressions. Nested assignments and variable increments/decrements/etc. are also not pure.

- a result of a function call

statement

- usually an imperative command

- usually indicate a side-effect (e.g. assignment, sending a message, etc.)

- can be a procedure call or macro

- can be composed out of expressions and nested statements, but with better defined semantics / order of evaluation

- a special case: control flow statements

- a special case: "return" statement - evaluate a value and returns it as a result of the current function

> statement

> - a special case: control flow statements

But it’s worth noting that this is not true in all languages: in some languages, almost everything or actually everything is an expression. In Rust, for example, control flow things may be statements or expressions (a mildly fuzzy distinction, but it comes down to whether it produces a value other than (), and whether it needs a semicolon after the closing curly brace), and some control flow things are very useful as expressions:

  let a = if b {
      c
  } else {
      d
  };

  let e = loop {
      break f;  // f is a value, not a label (which uses lifetime syntax, 'g).
  };
Ternary operator is an expression, first implemented as COND in Lisp.

In some cases it can be optimized to remove branching, especially when vectorized for SIMD/SIMT.

  int a,b,c,d;
  a = b ? c : d;

  // optimized to something like this:
  a = b*c + (1-b)*d

In some PLs even loop statements can produce values (CoffeeScript, Autocode), i.e. similar to list comprehensions, which are undoubtedly expressions.

I'm a kdb+/q developer, and most of the q code are expressions without conventional control flow operators (the only place they're used is outside of the main path: i.e. logging, testing, etc.).

IMO control flow (i.e. actual branching and looping) is just an implementation detail.

(comment deleted)
I think the simplest explanation is; "An expression can be used where a value is expected".

What is sometimes confusing, is that some statements also double as expressions, that is, also return values; and that it also depends on the language. eg. the assignment statement returns a value in C and Javascript, but not in Go or Python;

    printf("%d\n", a = 42);
    // out: 42

    console.log(a = 42)
    // out: 42

    fmt.Printf("%d\n", a = 42)
    // out: syntax error: unexpected =, expecting comma or )

    print("%d" % a = 42)
    # out: SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
Also depending on the language, expressions themselves may be valid statements. For example, this compiles in C;

    int main() {
        12 + 42;
    }
;this is a valid python program:

    12 + 42
;but if I try this in Go:

    package main
    func main() {
        42
    }
    // ./main.go:3:5: 12 + 42 (untyped int constant 54) is not used
Generally, an expression returns a value and can be used as the operand of an operator, whereas a "statement" is the terminal "top-level" node that doesn't return a value and cannot therefore be nested.
(comment deleted)
Expressions `yield` values, statements don't yield values.

`square = square(4)` is a statement (a definition to be exact) that associates name `square` with the value `square(4)`.

The name `square` itself, when later used, is an expression, because when you call it, it evaluates to a value `square(4)`.

For example, you might have tried writing something like this:

  foo = if (x < y) {
    ...
    ...
  }
Doesn't work because statements don't yield values.
Thats one of the reason why I REALLY like languages where everything is an expression.

    let foo = 
        if x < y 
            somevalue
        else
            someothervalue
in f# for example.
I also love F#. One of the most interesting languages that I've ever seen.

This is certainly another great aspect of it.

So are ternaries expressions then too?

  const x = x < y ? 2 : 3;
I would like to see a programming language where there is no distinction between the two. This might sound silly first, but once you start thinking about homoiconicity or about function approximations, like neural nets that can represent any function (as sort of a compressed lookup table) then you can start to imagine how a sufficiently advanced compiler should be able to inline approximate representations, or switch to calculating on the fly depending on needs and available resources.
Not sure if it was irony, but check out Haskell and alikes. Its execution model is different from a regular “it evaluates at where it is”. Infinite lists like all natural numbers are a norm in it.

  ones = 1 : ones
  take 3 ones // [1,1,1]

  odds = 1 : map (+2) odds
  take 3 odds // [1,3,5]
Haskell has a statement-like “do” notation, but it is a syntactic sugar for a structure of specific operators.

Lisp is also a beast to be aware of, though it is not exactly what you described.

Rust actually accomplishes this with the unit '()' type which is similar but not the same as void in other languages. Any statement (or expression) produces a value, sometimes that value is just a unit, which can be passed around just as any other value. Currently the exception to this are 'let' statements but there is a nightly feature where this produces a unit value as well.
> I think we often blame React for seemingly arbitrary rules, like how components must return a single top-level element. But more often than not, React is just warning us about a JavaScript limitation.

Really? With lit you can create a component that has several top-level elements[1].

EDIT: In fact this works even in react/preact with htm[2], so it's just a limitation of JSX.

[1]: https://lit.dev/playground/#project=W3sibmFtZSI6InNpbXBsZS1n...

[2]: https://github.com/developit/htm

Probably worth adding "(javascript)" to the HN title - for non-JS see @nivertech's excellent comment here.

I may be wrong but I actually think the explanation can be a little simpler than the article posits: an expression (in JS) is something that returns a value. That's it. The value can be undefined/void, but it's still a value being returned.

One important detail the article seems to miss that I've come across whie explaining JS syntax to JSX-learners is that, while statements usually contain expressions, expressions can also contain statements. This is because a function definition returns a value (the function instance) & obviously function bodies can contain any JS syntax. This is useful if you (for some odd reason) want to embed a statement in some JSX code (since JSX only supports JS expressions).

(comment deleted)
This is why taking a basic programming language course is pretty important. I never remembered the difference until I had to represent both in an AST. You can teach it without talking about ASTs or interpreters, but it's teaching without any context or motivation, thereby making it harder to learn.

I do think mandatory semicolons make the distinction a little easier. If it has a semicolon, it's probably a statement. And for most C-style languages if it has curly braces, it's probably a statement too. Otherwise it's an expression.

I feel like the examples and description for "statements" fails to demonstrate their breadth.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

In fairness it's difficult to talk about statements accurately and succinctly without being more abstract - and perhaps the more general observation here is how explanation by example alone is not always sufficient. A more general and yet tangible starting point for statements in JS could be "anything ending in a semicolon" (not quite true but almost for normal code), which at least is better at highlighting it's breadth before piling into arbitrary examples.

They should deprecate Array.prototype.forEach for muddling statements and expressions!
One of the nice things about Lisp is there are no statements, only expressions. It's easier to think about your code.

Lisp was the first language I used (other than assembly code) so when I encountered C I was puzzled why they even bothered to have statements at all. 40 years later I still don't understand the point.

Understand where you are coming from, but given the nature of C, what is the compiler to do with something like:

    1 2 3
which are three expressions, and probably the programmer meant "123". Of course:

    1; 2; 3;
is OK.
But that’s the point: what value does a language gain by making the distinction? It removes composability for no gain.
There is some gain in that you need to explicitly say that something is evaluatable, like:

   (+ 1 2)
as opposed to:

    1+2
Whether the gain is worth it is, I agree, debatable.
Lisp’s homoiconic representation is not relevant to this issue.

In C, `1+2` is an expression that returns 3. `1+2+3` acts like two expressions (and perhaps it is; I forget the precise terminology used in the standard). You can write `return foo(1+2)`.

But `for` introduces a statement so can only be used where a statement can.

Haskell is also based on an everything-is-an-expression syntax, but ended up re-inventing statements one level up, in the do-notation blocks.

To me, this indicates there is some merit to the statement/expression distinction.

I'd say Haskell didn't reinvent statements so much as it just modeled them. That allows manipulating them before they are interpreted, which is a good thing generally.
As someone who had recently started going the other direction, from C and C-based languages to those more influenced by lisp, it took a bit of getting used to but I’ve grown to appreciate the expressiveness and composability. The only thing I can think of that statements allow, but as I understand it pure expressions don’t, would be early returns. The closest replacement I’ve seen is to just make “return” raise an exception and wrap the whole expression with the try/catch equivalent, which isn’t terrible but was a bit surprising coming from a C-based background where returning early is a fairly common pattern.
You shouldn't need to raise exceptions for early returns. You can usually restructure your program to provide for the proper return without needing to use a return or comparable form when using an expression oriented language. If you had this C snippet, for instance:

  int foo(...) {
    if (condition) return an_expression;
    ...
    return another_expression;
  }
You'd put everything after that if into the else part instead of dropping it (like you would in C):

  (defun foo (...)
    (if condition
      an-expression
      (progn
        ...
        another-expression))) ;; and this /progn/ form would possibly be extracted to a new function if it was too long
Or more likely a cond if using Common Lisp and there were more than two cases:

  (defun foo (...)
    (cond (condition an-expression)
          ...
          (t
             ...
             another-expression)))
And in Common Lisp (and some others) you can still get a return form, using return-from in particular, from functions if you want to keep something more like the C-ish form:

  (defun foo (...)
    (when condition (return-from foo an-expression))
    ...
    another-expression))
Thanks, learned something new today with return-from! I’d be curious how different cl implementations implement it.

The first few examples are what I was more familiar with, and it is precisely your note “and this /progn/ form would possibly be extracted to a new function if it was too long” which makes me want to just return early in some cases. Sometimes a case is common enough that it warrants a separate function, but having to define functions for incredibly specific cases just to avoid excessive indentation is not ideal for me.

> Thanks, learned something new today with return-from! I’d be curious how different cl implementations implement it.

At least in SBCL, with it compiling the function, it turns into exactly the kind of assembly code you'd expect. Using disassemble:

    CL-USER> (defun foo (n)
               (when (< n 0) (return-from foo 20))
               (* 20 n))

    CL-USER> (disassemble #'foo)
    ; disassembly for FOO
    ; Size: 54 bytes. Origin: #x53642F41                          ; FOO
    ; 41:       498B4510         MOV RAX, [R13+16]                ; thread.binding-stack-pointer
    ; 45:       488945F8         MOV [RBP-8], RAX
    ; 49:       488B55F0         MOV RDX, [RBP-16]
    ; 4D:       31FF             XOR EDI, EDI
    ; 4F:       FF14252001A052   CALL QWORD PTR [#x52A00120]      ; SB-VM::GENERIC-<
    ; 56:       7C16             JL L1
    ; 58:       488B55F0         MOV RDX, [RBP-16]
    ; 5C:       BF28000000       MOV EDI, 40
    ; 61:       FF14251001A052   CALL QWORD PTR [#x52A00110]      ; SB-VM::GENERIC-*
    ; 68: L0:   488BE5           MOV RSP, RBP
    ; 6B:       F8               CLC
    ; 6C:       5D               POP RBP
    ; 6D:       C3               RET
    ; 6E: L1:   BA28000000       MOV EDX, 40
    ; 73:       EBF3             JMP L0
    ; 75:       CC10             INT3 16                          ; Invalid argument count trap
This sets up the call for the comparison, conditionally jumps based on the result (JL), and then returns one of the two results. Note that the "40" in 5C and 6E are really "20", this is a consequence of how many CL implementations handle integers, the low-order bit is used as a tag so not really part of the value. If you wrote it in C it would be very similar.

https://godbolt.org/z/cYPhdW3qd

The biggest difference is that the C version doesn't need a call for the comparison.

> (5 + 1) * 2

That’s six expressions, not five. Josh conflates the the addition expression and the parenthesised expression, but they’re distinct (even if you couldn’t represent the semantics without the parenthesis due to operator precedence).

Here’s an approximate representation of the parse tree, indicating which nodes are expressions:

  MultiplicativeExpression {        ←
    ParenthesizedExpression {       ←
      AdditiveExpression {          ←
        DecimalIntegerLiteral("5")  ←
        "+"
        DecimalIntegerLiteral("1")  ←
      }
    }
    "*"
    DecimalIntegerLiteral("2")      ←
  }
I am puzzled. 'statement' and 'expression' are items of JavaScript syntax (or C or Java or C++ or Perl or whatever). So to explain it, you point to the syntax definition.

This isn't going to be explained satisfactorily by analogies or intuition, and it won't be logical at all, because it isn't, and you could define the grammar with no such distinction or a different set of distinctions as many posters already showed here.

Besides being arbitrary, I also never found this a useful distinction after having programmed in languages that don't distinguish this. And look at how GCC introduced ({...}) to convert back a statement into an expression.

Anyway, to define this, you have to simply list all the things: statements are 'return', '{...}', 'while() {... }', etc., while expressions are '5', 'f(...)', etc. That's it, that's the explanation. No intuition, just a comprehensive list taken from the grammar.

If course there is some rough idea behind it, which explains why there is a difference, but this is a history class, not an explanation: statements do some action, i.e., this is an imperative concept. Expressions have a value, i.e., this is a functional concept.

I've been reading this on functional JS:

https://news.ycombinator.com/item?id=31944352

For example, functions that return functions:

const filter = (predicate, xs) => xs.filter(predicate)

So it's an assignment, leading me to think it's a statement, but then using filter later would be an expression?

That example is particularly confusing because it’s a function definition written as an assignment for IMO no good reason. The outcome is the same either way, but

  function filter(predicate, xs) { return xs.filter(predicate) }
lets you know immediately that “filter” is a function, not a value. There are valid cases where assigning an anonymous function a name via “const” is appropriate, for example when working with higher-order functions, but most of the time it’s not necessary or helpful. I wish it hadn’t caught on.

EDIT: I should have read your comment more carefully. The example function is not a HOF though, it runs xs.filter(pred) when called. A HOF would be

  function addConst(x) ( return (y) => x + y }
  const add3 = addConst(3)
  add3(5) // returns 8
The second line can still be written as

  function add3(x) { return addConst(3) }
I think people use the const form because there’s less text to write. Syntax highlighting makes this form more bearable, but I still don’t like it.
Something that clicked for me recently in JS and specifically React was why we use {} for interpolation in a component. So an example:

  const MyComponent = () => {
    return (
      <>
        <h1>Hello world</h1> 
        { arr.map(val => <ListItem val={val} />) }
        <h1>Goodbye world</h1> 
      </>
    );
  }
Curly braces often denote expressions. They're everywhere:

In functions:

  const x = () => {

  };
  // or
  function myFunc() { 
  
  };
In conditionals (while the conditionals are statements their bodies are expressions)

  if (x > 2) {
    return true;
  }
In Loops

  while (x > 2) {
    doSomething();
  }
etc.

This is obviously part of the language grammar of many languages and makes it obvious why React uses curly braces as well for interpolating values.