Skip to main content

Differences between arrow Function, Function declaration and Function expression

Function declarations, function expressions, and arrow functions are three ways to define a function in JavaScript, each behaving differently around hoisting, this binding, and constructor support.

Theory

TL;DR

  • Function declarations hoist fully - call them anywhere in their scope, even before they appear in the file
  • Function expressions and arrow functions stay in the TDZ until assignment - accessing them early throws a ReferenceError
  • Arrow functions have no own this - they capture it lexically from the surrounding scope at definition time
  • Arrow functions can't be used with new - they have no [[Construct]] internal method
  • Rule of thumb: declarations for top-level utilities, arrows for callbacks and handlers, expressions for IIFEs

Quick example

javascript
// Declaration: hoisted, callable before definition sayHi(); // 'Hi!' - no error function sayHi() { console.log('Hi!'); } // Expression: TDZ with const - calling early throws // sayHiExpr(); // ReferenceError const sayHiExpr = function() { console.log('Hi from expression!'); }; // Arrow: lexical this - takes it from surrounding scope const obj = { name: 'World', greet: () => console.log('Hi ' + this.name) // undefined, not 'World' }; obj.greet(); // 'Hi undefined'

Only the declaration works before its line. The arrow's this.name is undefined because the arrow captured this from the outer module scope, not from obj.

Key difference

The split that matters is hoisting. Function declarations are parsed before any code runs - the entire body lands in the execution context's VariableEnvironment upfront, making the function callable from line one. Expressions and arrows only hoist the variable binding: undefined with var, or a TDZ block with const/let. Beyond that, arrows skip creating this and arguments entirely - both are inherited from the enclosing lexical scope via the scope chain.

When to use

  • Top-level utility function needed anywhere in the file - function declaration
  • Callback, array method, or event handler - arrow function (short syntax, correct this)
  • IIFE or higher-order return where you want a named function in stack traces - function expression
  • Constructor called with new - function declaration or expression, never arrow
  • Event listener in a class component - arrow (binds this to the instance automatically)

Comparison table

CharacteristicFunction DeclarationFunction ExpressionArrow Function
Syntaxfunction name() {}const name = function() {}const name = () => {}
HoistingFull: entire bodyVariable only (TDZ with const/let)Variable only (TDZ with const/let)
this bindingDynamic (call-site)Dynamic (call-site)Lexical (enclosing scope)
arguments objectYesYesNo - use rest params (...args)
Constructor (new)YesYesNo - throws TypeError
super in classesYesYesNo
When to useHoisted utilities, module exportsIIFEs, higher-order returnsCallbacks, map/filter, React handlers

How V8 handles this

V8 parses declarations in the pre-execution phase, allocating the full function object in the VariableEnvironment before a single line runs. Expressions and arrows parse as literals during execution and bind to the declared variable only when that line is reached. Arrows skip creating this and arguments bindings entirely - when you reference this inside an arrow, the engine walks up the scope chain to find it in the enclosing LexicalEnvironment.

Common mistakes

Calling an arrow before assignment:

javascript
greet(); // ReferenceError: Cannot access 'greet' before initialization const greet = () => console.log('hi');

const keeps greet in the TDZ. Move the call below the assignment, or use a function declaration.

Expecting dynamic this from an arrow method:

javascript
const obj = { name: 'Alice', say: () => console.log(this.name) // undefined, not 'Alice' }; obj.say();

The arrow grabbed this from the module scope, not from obj. Use a regular method shorthand: say() { console.log(this.name); }.

Using arguments inside an arrow:

javascript
const sum = () => console.log(arguments); // ReferenceError or outer arguments sum(1, 2, 3);

Arrows have no arguments binding. Use rest parameters: const sum = (...args) => args.reduce((a, b) => a + b, 0).

Arrow as constructor:

javascript
const User = () => ({ name: 'Bob' }); new User(); // TypeError: User is not a constructor

No [[Construct]] means no new. Use function User() {} or class User {}. This one shows up in code reviews more often than you'd expect - usually when someone refactors a class method into an arrow and forgets to update the callers.

super in an arrow class field:

javascript
class Child extends Parent { // arrow = () => super.method(); // SyntaxError method() { super.method(); } // correct }

Real-world usage

  • React - arrow class fields (handleSubmit = () => {}) to bind this to the instance without .bind(this) in the constructor
  • Express.js - arrows in route callbacks (app.use((req, res, next) => {})) for predictable this
  • Redux - arrow selectors (const getUsers = state => state.users)
  • Node.js streams - function declarations for reusable parsers called from multiple places
  • Lodash - named function expressions in _.curry(function add(a, b) {}) so recursive calls work and stack traces stay readable

Follow-up questions

Q: What happens when a function expression is declared with var instead of const?
A: The variable hoists as undefined. Calling it before the assignment line gives TypeError: sayHi is not a function - not a ReferenceError - because the variable exists but holds undefined at that point.

Q: Why does arrow have no prototype?
A: The spec doesn't allocate prototype on arrows because they can't use [[Construct]]. No reason to create a property that would never be used.

Q: Does this change in a function declaration under strict mode?
A: Yes. In strict mode, this inside a function called without an explicit receiver is undefined, not the global object. Arrows are unaffected since they don't create their own this.

Q: Can you give an arrow function a name for better stack traces?
A: When you assign an arrow to a named const, the engine infers that variable name as the function name and shows it in stack traces. But you can't write a named arrow directly the way you write function log() {}.

Q: (Senior) In V8, how does TDZ behave differently for an immediately-invoked arrow with const vs a var function expression?
A: const f = (() => {})() evaluates fine on that line - TDZ only blocks access to f before the line, not the expression itself. With var f = (function(){})(), the var hoists as undefined. Referencing f before the line returns undefined, and calling f() throws TypeError: f is not a function - a different error from TDZ's ReferenceError, which matters when debugging.

Examples

Hoisting in practice

javascript
// Declaration is available immediately console.log(add(2, 3)); // 5 function add(a, b) { return a + b; } // Expression with const: TDZ until assignment line // console.log(multiply(2, 3)); // ReferenceError const multiply = function(a, b) { return a * b; }; console.log(multiply(2, 3)); // 6

add works on line 2 because the declaration was hoisted with its full body. The commented multiply call would throw - const keeps the binding in the TDZ until the assignment.

Arrow function this in a React class component

javascript
class Button extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; // Expression: needs .bind(this) to get the right context this.handleClickExpr = function() { this.setState({ count: this.state.count + 1 }); }.bind(this); } // Arrow: captures this from class body, no bind needed handleClickArrow = () => { this.setState({ count: this.state.count + 1 }); }; render() { return <button onClick={this.handleClickArrow}>{this.state.count}</button>; } }

The expression version needs .bind(this) or it loses context when the browser calls it. The arrow captures this from the class definition scope automatically. This is the pattern pre-hooks React relied on for class event handlers.

Arrow fails as constructor in a factory

javascript
function Decl() { this.type = 'declaration'; } const Expr = function() { this.type = 'expression'; }; const Arrow = () => {}; new Decl(); // { type: 'declaration' } new Expr(); // { type: 'expression' } new Arrow(); // TypeError: Arrow is not a constructor // Real-world pattern that breaks at runtime, not compile time function createInstance(Ctor) { return new Ctor(); } createInstance(Decl); // works createInstance(Arrow); // TypeError in production

new Arrow() throws because arrows have no [[Construct]] method. The dangerous part: there's no compile-time error. If you pass an arrow to a factory expecting a constructor, the crash surfaces at runtime.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?