What is IIFE (immediately invoked Function expression) in JavaScript
IIFE (Immediately Invoked Function Expression) is a function that defines and runs itself in one shot, creating a private scope that disappears after execution.
Theory
TL;DR
- IIFE is like a soundproof booth: code runs inside, variables stay inside, nothing leaks to global scope
- Main difference from regular functions: runs once automatically when the parser hits it, not when you call it
- Use it for one-time init code, private variables, or passing globals safely into a contained scope
- Post-ES6:
import/exportandletin blocks cover most cases, but IIFE still appears in legacy code and Babel output
Quick example
// Without IIFE - var leaks to global scope
var count = 0;
console.log(count); // 0 - visible everywhere
// With IIFE - scope stays contained
(function() {
var count = 0;
count++;
console.log(count); // 1
})();
console.log(typeof count); // "undefined" - didn't leakThe outer parens turn a function declaration into an expression. The trailing () invoke it immediately. That's the whole syntax.
How scope isolation works
When the JavaScript engine parses an IIFE, it creates a new execution context with its own variable environment. The function runs, variables live inside that context, and when it finishes the context is discarded. Variables die with it. Nothing reaches the outer scope unless you explicitly return or assign something outward.
This is also why var inside an IIFE doesn't pollute window: the variable exists in the function's scope, not the global one. The same mechanism that powers closures is at work here.
When to use
- One-time initialization (config setup, polyfill loading) - run once, no lingering variables
- Passing globals safely:
(function($) { ... })(jQuery)pins the$alias regardless of external conflicts - Legacy module patterns like AMD/UMD wrappers, before ES6 modules were available
- Wrapping third-party scripts where you can't control the global namespace
Skip it when you have ES6 modules (import/export), or when you just need block scope ({ let x = 1; }).
How the engine handles it
The outer parens tell the parser: this is an expression, not a declaration. Without them, function() {}() throws a SyntaxError because the parser expects a name after function. V8 then adds the IIFE to the call stack, runs it in a fresh execution context, and pops it off. The scope chain it created is gone. Any closures inside (like setTimeout callbacks) keep a reference to that scope alive, but no outside code can reach it directly.
Common mistakes
Forgetting the outer parens:
function() { console.log("hi"); }(); // SyntaxErrorThe parser sees a function declaration without a name. Fix: wrap in parens (function() { ... })();
Assuming IIFE prevents all leaks:
(function() {
notDefined = 1; // No var/let/const!
})();
console.log(notDefined); // 1 - leaked to globalWithout var, let, or const, the assignment creates a global. Add "use strict" as the first line inside the IIFE to catch this.
Recursive IIFE using arguments.callee:
// Broken in strict mode
var fib = (function(n) {
if (n <= 1) return n;
return arguments.callee(n-1) + arguments.callee(n-2); // Deprecated
})(10);
// Fix: use a named function expression
var fib = (function f(n) {
return n <= 1 ? n : f(n-1) + f(n-2);
})(10);arguments.callee is blocked in strict mode. The named function expression f refers to itself without that problem.
Using IIFE for loop closures when let exists:
// Old way - IIFE to capture loop variable
for (var i = 0; i < 3; i++) {
(function(j) { setTimeout(() => console.log(j), 0); })(i);
}
// Modern way - just use let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}Real-world usage
- jQuery plugins:
(function($) { $.fn.myWidget = function() { ... }; })(jQuery)was the standard pattern for years - Lodash/Underscore source: IIFE passes
rootandfreeGlobalfor AMD/CommonJS detection - Babel output: still wraps transpiled modules in IIFE for browser scope isolation
- Polyfill scripts:
(function() { if (!window.Promise) { /* load polyfill */ } })();
I saw this pattern constantly in pre-ES6 codebases. Once ES6 modules landed in browsers, new projects stopped writing IIFEs, but maintenance work still means reading them.
Follow-up questions
Q: Write an IIFE that returns a counter function.
A: var counter = (function() { var count = 0; return function() { return ++count; }; })(); The IIFE runs once and returns the inner function. count stays alive because the returned function closes over it: counter() returns 1, then 2, and so on.
Q: Why did IIFEs matter before ES6?
A: var has function scope, not block scope. In large apps with many script files, every top-level var became a property on window. IIFE was the only reliable way to create a private scope without a module system.
Q: What's the difference between arrow IIFE and classic IIFE?
A: (() => { ... })() works for most cases. Arrow functions don't bind their own this or arguments, so the classic function form is needed when the code inside relies on those.
Q: A setTimeout callback inside an IIFE runs after the IIFE finishes. Does it keep the scope alive?
A: Yes. The callback closes over the IIFE's scope. The execution context is gone, but the closure keeps the variable bindings alive until the callback runs and gets garbage collected.
Q: (Senior) How would you write a UMD wrapper using the IIFE pattern for a no-bundler environment?
A: (function(root, factory) { if (typeof define === 'function') define(factory); else root.MyLib = factory(); })(this, function() { return { /* public API */ }; }); It detects AMD at runtime and falls back to attaching the library to global scope directly.
Examples
Basic: scope isolation
(function() {
var privateVar = "I exist only here";
console.log(privateVar); // "I exist only here"
})();
console.log(typeof privateVar); // "undefined" - gone after IIFE ranThe variable is created, used, and discarded when the IIFE's execution context is destroyed. No trace in global scope.
Real-world: jQuery plugin with safe $ alias
// Passing jQuery as an argument fixes the $ alias
// even if another library also claims $
(function($) {
$.fn.tooltip = function(options) {
return this.each(function() {
var el = $(this);
el.attr("title", options.text || "default");
});
};
})(jQuery);
// Usage
$(".btn").tooltip({ text: "Click me" });This is the pattern DataTables, Select2, and most jQuery plugins used. The $ inside the IIFE always refers to jQuery, regardless of what $ means in the outer scope. The argument injection pattern is what made this safe across mixed codebases.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.