DEV Community

Cover image for Types of Functions in JavaScript — Differences Explained with Examples
Amit Kumar
Amit Kumar

Posted on

Types of Functions in JavaScript — Differences Explained with Examples

Types of Functions in JavaScript — Differences Explained

JavaScript has several ways to define functions. They look similar, but they differ in hoisting, this, arguments, and whether they can be used with new.

If you’ve ever wondered why fn() works before its line for one function but throws for another — or why an arrow “method” prints undefined — this guide is for you.

Table of contents
  1. Function Declaration
  2. Function Expression
  3. Arrow Function
  4. Object Method
  5. Constructor Function & Class
  6. Generator Function
  7. Async Function
  8. IIFE (Immediately Invoked Function Expression)
  9. Quick Comparison Table
  10. Key Differences
  11. When to Use What
  12. Interview Gotchas


1. Function Declaration

function greet(name) {
  return `Hello, ${name}`;
}

greet("Amit"); // "Hello, Amit"
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Fully hoisted — you can call it before its definition
  • Has its own this (depends on how it’s called)
  • Has arguments
  • Can be used with new, but prefer class / dedicated constructors — don’t treat that as the normal pattern
  • Best for named, reusable top-level functions
sayHi(); // works — hoisted

function sayHi() {
  console.log("Hi");
}
Enter fullscreen mode Exit fullscreen mode

2. Function Expression

const greet = function (name) {
  return `Hello, ${name}`;
};
Enter fullscreen mode Exit fullscreen mode

Named function expression (useful for stack traces / recursion):

const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1);
};
Enter fullscreen mode Exit fullscreen mode

Key traits

  • The variable may be hoisted (var), but the function value is not available until the assignment runs
  • With const / let, accessing before the line is a TDZ ReferenceError
  • With var, calling before assignment is usually a TypeError (undefined is not a function)
  • Same this / arguments behavior as a declaration
  • Can be anonymous or named
  • Common when assigning functions to variables or passing as callbacks
// var → TypeError
sayHiVar(); // TypeError: sayHiVar is not a function
var sayHiVar = function () {
  console.log("Hi");
};

// const / let → ReferenceError (TDZ)
sayHiConst(); // ReferenceError: Cannot access 'sayHiConst' before initialization
const sayHiConst = function () {
  console.log("Hi");
};
Enter fullscreen mode Exit fullscreen mode

3. Arrow Function

const greet = (name) => `Hello, ${name}`;

const add = (a, b) => {
  return a + b;
};
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Not hoisted
  • No own this — uses lexical this from the surrounding scope
  • No arguments object (use rest params: (...args) => {})
  • Cannot be used as a constructor (new throws)
  • No prototype
  • Perfect for callbacks, map / filter / reduce, and preserving outer this

Lexical this example

const user = {
  name: "Amit",
  regular() {
    setTimeout(function () {
      console.log(this.name); // undefined (or global) — lost this
    }, 0);
  },
  arrow() {
    setTimeout(() => {
      console.log(this.name); // "Amit" — lexical this
    }, 0);
  },
};

user.regular();
user.arrow();
Enter fullscreen mode Exit fullscreen mode

Rest instead of arguments

const sum = (...nums) => nums.reduce((a, b) => a + b, 0);
sum(1, 2, 3); // 6
Enter fullscreen mode Exit fullscreen mode

Guess the output?
const user = {
  name: "Amit",
  greet: () => this.name,
};
console.log(user.greet());
Enter fullscreen mode Exit fullscreen mode

Answer: undefined in a typical module / strict runtime — the arrow does not take this from user.



4. Object Method

const user = {
  name: "Amit",
  greet() {
    return `Hello, ${this.name}`;
  },
};

user.greet(); // "Hello, Amit"
Enter fullscreen mode Exit fullscreen mode

Key traits

  • this is usually the object that owns the method (when called as obj.method())
  • Shorthand method syntax is preferred over greet: function () {}
  • Avoid arrow functions as object methods if you need this to be the object
const user = {
  name: "Amit",
  greet: () => `Hello, ${this.name}`, // wrong — lexical this
};

user.greet(); // "Hello, undefined" (in modules / strict mode)
Enter fullscreen mode Exit fullscreen mode

5. Constructor Function & Class

Constructor function

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function () {
  return `Hello, ${this.name}`;
};

const p = new Person("Amit");
p.greet(); // "Hello, Amit"
Enter fullscreen mode Exit fullscreen mode

Class (modern syntax)

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, ${this.name}`;
  }
}

const p = new Person("Amit");
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Meant to be called with new
  • Creates instance objects
  • Methods usually live on the prototype (shared across instances)
  • Prefer class in modern code; behavior is still prototype-based under the hood
  • Classes are hoisted into the TDZ — you cannot use them before the class line (ReferenceError)
const user = new User(); // ReferenceError
class User {}
Enter fullscreen mode Exit fullscreen mode

React / class-fields note

class Button {
  label = "Save";

  // Per-instance arrow — lexical this (handy for callbacks)
  onPress = () => this.label;

  // Prototype method — this depends on call site
  handle() {
    return this.label;
  }
}
Enter fullscreen mode Exit fullscreen mode

6. Generator Function

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

const gen = idGenerator();
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Defined with function*
  • Can pause and resume with yield
  • Returns an iterator
  • Useful for lazy sequences, custom iteration, infinite lists

Also works as a generator method:

const obj = {
  *range(n) {
    for (let i = 1; i <= n; i++) yield i;
  },
};

[...obj.range(3)]; // [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

There are also async generators (async function*) used with for await...of — same pause/resume idea for async streams.


7. Async Function

async function loadUser() {
  const res = await fetch("/api/user");
  return res.json();
}

// async arrow
const loadUserArrow = async () => {
  const res = await fetch("/api/user");
  return res.json();
};
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Always returns a Promise
  • Lets you write async code with await in a sync-looking style
  • Thrown errors become rejected promises (use try/catch or .catch())
  • await on a non-Promise still works — the value is wrapped
async function example() {
  return 42;
}

example(); // Promise that resolves to 42
Enter fullscreen mode Exit fullscreen mode

8. IIFE (Immediately Invoked Function Expression)

Also called a self-calling or self-invoking function.

An IIFE is defined and executed in the same step — you don’t call it later by name.

(function () {
  console.log("I run immediately");
})();

// Arrow version
(() => {
  console.log("I also run immediately");
})();
Enter fullscreen mode Exit fullscreen mode

Why wrap in parentheses?

Without wrapping, JS treats function as a declaration, and declarations can’t be invoked that way as a statement.

// Valid — force an expression, then invoke
(function () {
  console.log("works");
})();
Enter fullscreen mode Exit fullscreen mode

Other (less common) IIFE forms
(function () {
  console.log("form 1");
})();

(function () {
  console.log("form 2");
}());

!function () {
  console.log("form 3");
}();
Enter fullscreen mode Exit fullscreen mode

Prefer the (function () { ... })() or (() => { ... })() style in real code.


Pass arguments

(function (name) {
  console.log(`Hello, ${name}`);
})("Amit");
Enter fullscreen mode Exit fullscreen mode

Create private scope

(function () {
  const secret = "hidden";
  // secret is not accessible outside
})();

// console.log(secret); // ReferenceError
Enter fullscreen mode Exit fullscreen mode

Return a value / module-like API

const counter = (function () {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    getCount() {
      return count;
    },
  };
})();

counter.increment(); // 1
counter.getCount(); // 1
// count itself is private
Enter fullscreen mode Exit fullscreen mode

Async IIFE

Useful for top-level async setup when you can’t use top-level await:

(async function () {
  const res = await fetch("/api/user");
  console.log(await res.json());
})();
Enter fullscreen mode Exit fullscreen mode

Key traits

  • Defined and executed immediately
  • Creates a private scope
  • Can take parameters and return values
  • Common before ES modules; still used in bundles, polyfills, one-off setup, and interviews

Guess the output?
const result = (function () {
  var x = 10;
  return x;
})();

console.log(result);
// console.log(x);
Enter fullscreen mode Exit fullscreen mode

Answer: result is 10. Logging x throws ReferenceErrorx stayed private inside the IIFE.



9. Quick Comparison Table

Feature Declaration Expression Arrow Method Constructor / Class Generator Async
Hoisting Yes (full) No (assignment); var/const differ No No Function ctor: yes · Class: TDZ Like its form Like its form
Own this Yes Yes No (lexical) Yes (usually) Yes (instance) Yes Yes (unless arrow)
arguments Yes Yes No Yes Yes Yes Yes (unless arrow)
Can use new Possible* Possible* No Not intended Yes Avoid No
Returns Any Any Any Any Instance Iterator Promise
Best for Named utils Assigned fns Callbacks Object behavior OOP / instances Lazy iteration Async / await

* Technically allowed for classic functions, but prefer class for constructors.


10. Key Differences

Declaration vs Expression

Point Function Declaration Function Expression
Syntax function fn() {} const fn = function () {}
Before definition Callable varTypeError · const/letReferenceError
Name required Yes Optional
Use case Top-level helpers Assign to vars, pass as values
hello(); // works
function hello() {
  console.log("declaration");
}

bye(); // ReferenceError with const
const bye = function () {
  console.log("expression");
};
Enter fullscreen mode Exit fullscreen mode

Arrow Function vs Normal Function

“Normal function” = declaration or classic expression.

Feature Normal Function Arrow Function
Own this Yes — depends on how it’s called No — lexical
arguments Yes No — use ...args
Can use new Yes No (TypeError)
prototype Yes No
Implicit return No Yes — () => value
Best for Methods, constructors, own this Callbacks, keep outer this

this binding

const user = {
  name: "Amit",
  normal: function () {
    console.log(this.name); // "Amit"
  },
  arrow: () => {
    console.log(this.name); // undefined (not the object)
  },
};

user.normal();
user.arrow();
Enter fullscreen mode Exit fullscreen mode

Constructor usage

function Person(name) {
  this.name = name;
}
new Person("Amit"); // OK

const PersonArrow = (name) => {
  this.name = name;
};
new PersonArrow("Amit"); // TypeError
Enter fullscreen mode Exit fullscreen mode

When to use which

  • Normal function → object methods, constructors, handlers where you need call-site this
  • Arrow function → callbacks (map, filter, setTimeout) where you want the parent’s this

Method vs Arrow as Method

Point Method greet() {} Arrow property greet: () => {}
this Owning object (usual call) Outer / lexical this
Recommended for objects? Yes Usually no
const user = {
  name: "Amit",
  method() {
    return this.name; // "Amit"
  },
  arrow: () => this.name, // wrong for object method
};
Enter fullscreen mode Exit fullscreen mode

Constructor Function vs Class

Point Constructor Function Class
Syntax function Person() {} class Person {}
Style Older / prototype style Modern syntactic sugar
Before definition Function is hoisted TDZReferenceError
Internals Prototype model Same prototype model
function PersonFn(name) {
  this.name = name;
}

class PersonClass {
  constructor(name) {
    this.name = name;
  }
}
Enter fullscreen mode Exit fullscreen mode

Function Expression vs IIFE

Point Function Expression IIFE
When it runs When you call it later Immediately
Stored in variable? Usually yes Often not needed
Purpose Reuse later One-time run + private scope
const greet = function (name) {
  console.log(name);
};
greet("Amit");

(function (name) {
  console.log(name);
})("Amit");
Enter fullscreen mode Exit fullscreen mode

An IIFE can be an arrow:

(() => {
  console.log("arrow IIFE");
})();
Enter fullscreen mode Exit fullscreen mode

Normal vs Async vs Generator (quick)

Point Normal Async Generator
Keyword function async function function*
Returns Any value Always a Promise An iterator
Special await yield / .next()
function syncFn() {
  return 10; // number
}

async function asyncFn() {
  return 10; // Promise → 10
}

function* gen() {
  yield 1;
  yield 2;
}
Enter fullscreen mode Exit fullscreen mode

11. When to Use What

Need Prefer
Named reusable helper Function declaration
Pass / assign a function Function expression or arrow
Callback / array method / keep outer this Arrow function
Behavior on an object Object method / class method
Create many similar objects class
Pause / resume / lazy values Generator
Fetch / timers / promises Async function
Run once + private variables IIFE (or an ES module)

12. Interview Gotchas

1. Hoisting: declaration vs expression

fnDecl(); // works

function fnDecl() {}

fnVar(); // TypeError
var fnVar = function () {};

fnConst(); // ReferenceError (TDZ)
const fnConst = function () {};
Enter fullscreen mode Exit fullscreen mode

2. Arrow functions don’t bind their own this

const button = {
  id: "save",
  bindRegular() {
    setTimeout(function () {
      console.log(this.id); // undefined — lost this
    }, 0);
  },
  bindArrow() {
    setTimeout(() => {
      console.log(this.id); // "save" — lexical this
    }, 0);
  },
};
Enter fullscreen mode Exit fullscreen mode

In the browser, a regular addEventListener callback gets the element as this; an arrow does not.

3. Losing this when extracting a method

const user = {
  name: "Amit",
  greet() {
    return this.name;
  },
};

const fn = user.greet;
fn(); // undefined (this is lost)

fn.call(user); // "Amit"
Enter fullscreen mode Exit fullscreen mode

4. Async always returns a Promise

async function getValue() {
  return 10;
}

getValue().then(console.log); // 10
Enter fullscreen mode Exit fullscreen mode

5. Don’t use arrow functions as constructors

const Person = (name) => {
  this.name = name;
};

new Person("Amit"); // TypeError
Enter fullscreen mode Exit fullscreen mode

6. IIFE creates private scope

(function () {
  var x = 10;
})();

// console.log(x); // ReferenceError
Enter fullscreen mode Exit fullscreen mode

Practical Rule of Thumb

  • Declaration → top-level named functions
  • Arrow → callbacks and short logic where lexical this helps
  • Method / class → object behavior and OOP
  • Async → anything that waits on promises
  • Generator → when you need yield or custom iterators
  • IIFE → run once immediately with private scope (or just use a module)

Master these differences and you can explain not just how to write a function, but why one form is better than another — in interviews and in real projects.

If this helped, leave a ❤️ and comment which function type still confuses you the most.

Top comments (0)