#Function declarations vs expressions vs arrows
JavaScript gives you three everyday ways to make a function, and they are not just style choices. A function declaration (function greet() {}) is a statement on its own line that creates a function and a binding with its name. A function expression (const greet = function () {}) creates a function as a value in the middle of another statement, usually an assignment. An arrow function (const greet = () => {}) is a shorter expression syntax that also changes how this, arguments and new behave.
The big practical difference between a declaration and an expression is hoisting. Before any line of a scope runs, the engine scans that scope and creates every binding declared in it. A function declaration is created complete — name and body — so you can call it above the line where it's written. A var is created too, but holding undefined; the function only gets assigned when execution reaches that line. let and const bindings are created but left uninitialised (the temporal dead zone, TDZ) until their line runs, and touching them before that throws.
Declaration — hoisted with its body
greet(); // "Hello!"
function greet() {
console.log("Hello!");
}- Before line 1 runs, the setup phase creates the binding
greetand puts the whole function in it. greet()on line 1 finds a real function and logs"Hello!".- When execution reaches the
function greetline there is nothing left to do — it was handled during setup.
Expression — only the variable is hoisted
greet(); // TypeError: greet is not a function
var greet = function () {
console.log("Hello!");
};- During setup,
var greetcreates a binding holdingundefined. The function itself doesn't exist yet — it's created only when the= function …line runs. greet()readsgreet→undefined, then tries to call it. Calling a non-function is a TypeError.- Any call placed after the assignment line would work, because by then
greetholds the function.
With var, greet exists but is undefined at the call, so you get a TypeError. With let/const you'd get a ReferenceError (TDZ) instead: Cannot access 'greet' before initialization. The different error type is a useful clue when debugging — TypeError means "the name exists but holds the wrong thing", ReferenceError means "you can't use this name here at all".
Which should you pick? Declarations suit named, top-level helpers: hoisting lets you put the main logic at the top of a file and the helpers underneath. Expressions (usually const plus an arrow) suit functions that are values — passed as callbacks, stored in objects, or chosen conditionally — and const stops the name being reassigned by accident.
Arrow functions are a shorter expression syntax with real behavioural differences. They were added in ES2015 mainly to make small callbacks lighter and to stop callbacks losing this:
| Regular function | Arrow function | |
|---|---|---|
Own this | Yes — decided by how it's called | No — uses this of the surrounding scope |
arguments object | Yes | No (use ...args) |
Can be used with new | Yes | No — TypeError |
Has prototype | Yes | No |
| Good for object methods | Yes | Usually no |
| Good for callbacks | Needs bind if it uses this | Yes |
The this and arguments rows are the same rule: an arrow has no binding of its own, so the name is looked up in the enclosing function, exactly like a normal variable.
function outer() {
const arrow = () => arguments[0];
return arrow("arrow's own arg");
}
outer("outer's arg"); // "outer's arg"
const Arrow = () => {};
new Arrow(); // TypeError: Arrow is not a constructor
Arrow.prototype; // undefinedouter("outer's arg")runs. As a regular function it gets its ownargumentsobject:["outer's arg"].- Inside,
arrow("arrow's own arg")is called. The arrow has noargumentsof its own, soarguments[0]is resolved up the scope chain and findsouter'sarguments→"outer's arg". The value passed to the arrow is simply ignored. new Arrow()throws because arrows have no internal[[Construct]]behaviour — they can't create objects, sonewrefuses them up front.Arrow.prototypeisundefinedbecause aprototypeobject is only useful for constructors, and arrows can never be one.- This shows the theme of arrows: fewer bindings of their own (
this,arguments,prototype), which makes them ideal for callbacks and a poor fit for methods and constructors.
const square = (n) => n * n; // implicit return
const makeUser = (name) => ({ name }); // wrap object literals in ( )squarehas a concise body: no braces after=>, so the expressionn * nis returned automatically.square(4)→16.- In
makeUser, a{straight after=>would be read as the start of a block body, not an object.(name) => { name }is a block containing the useless statementname;, so it returnsundefined. - The
( )forces the parser to read{ name }as an expression — an object literal using shorthand — somakeUser("Rohit")→{ name: "Rohit" }.