In the last article, I introduced the try-catch IIFE pattern. Some of you found it clever, while others couldn’t stand the syntax. And honestly? Fair. IIFE (Immediately Invoked Function Expression) can look odd at first glance.
But I’m here to change your mind.
IIFE isn’t just a relic of the var
era, there are still many modern use cases.
Here are 7 modern use cases of IIFE that go beyond legacy code and show how they can improve clarity, encapsulation, and expressiveness in your everyday code.
⏳ What Is an IIFE? (And Why Was It Essential in the var
Era?)
Before we look at modern use cases, let’s rewind a bit.
IIFE stands for Immediately Invoked Function Expression. It's a function that runs right after it’s defined. Like this:
(() => {
console.log("Hello!");
})();
You define a function and immediately invoke it. But why go through this strange ritual?
Well, back in the var
era, var
was the only way to define variables. It is function-scoped, unlike const
and let
which are block-scoped. So, variables declared inside for
loops, if
/ switch
statements weren’t isolated — they leaked into the surrounding scope.
Here’s an infamous example:
var buttons = [];
for (var i = 0; i < 3; i++) {
buttons[i] = function () {
console.log(i);
};
}
buttons[0](); // 3
buttons[1](); // 3
buttons[2](); // 3
Surprise! They all log 3. That’s because var i
was shared across all iterations!
To fix this, devs discovered a clever trick: wrap each loop iteration in an IIFE:
for (var i = 0; i < 3; i++) {
(function (lockedInIndex) {
buttons[lockedInIndex] = function () {
console.log(lockedInIndex);
};
})(i);
}
Now it works as expected:
buttons[0](); // 0
buttons[1](); // 1
buttons[2](); // 2
This worked because each IIFE created a new function scope, capturing the correct value of i
in each iteration.
Ben Alman wrote a seminal article on this pattern back in 2010 — it was so important that he helped define the term IIFE and documented all the ways to write one correctly. Check out his article here.
He even clarified the distinction between function declarations and expressions, which tripped up countless developers at the time. The
()
around the function weren’t for nothing! They were crucial so that the function is treated as an expression, allowing it to be executed immediately.
In short:
- Without
let
andconst
, IIFE was the only way to get around problems with function scoping. - It became a powerful tool for encapsulation, module patterns, closures, and safe variable use.
Nowadays, we have proper block scoping via let
and const
. So IIFE has started to fade out from our memories.
...but that doesn’t mean it’s useless. As you’ll see below, IIFE still unlocks structure, encapsulation, and readability in surprisingly modern contexts.
Let’s dive in.
Use Case 1: Enable async/await
Ever tried using await
in a place where you're not allowed to write async
directly, like inside the useEffect
hook in React?
If you've ever tried to do something like this:
useEffect(async () => {
const response = await MyAPI.getData(someId);
// ...
});
You are probably no stranger to this error message:
The recommended way to fix this is to define an async function in your effect and call it immediate. And guess what a function that is called immediately is? An IIFE!
The example code in the error message recommends you to do:
useEffect(() => {
const fetchData = async () => {
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
});
But why bother naming the function that is only used once? Naming is well-known to be one of the hardest thing in programming, if not the hardest. So do yourself a favour, skip the naming and just use IIFE:
useEffect(() => {
(async () => {
const response = await MyAPI.getData(someId);
// ...
})();
});
Use Case 2: const
with switch
Sometimes switch
statements are used just to initialise a variable.
Let's check out this real example from React:
let postTaskPriority;
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
postTaskPriority = 'user-blocking';
break;
case LowPriority:
case NormalPriority:
postTaskPriority = 'user-visible';
break;
case IdlePriority:
postTaskPriority = 'background';
break;
default:
postTaskPriority = 'user-visible';
break;
}
// `postTaskPriority` is never reassigned again, but it is a `let`! 😢
The variable is never reassigned, yet we’re forced to use let
because the value is assigned inside the switch
.
With IIFE:
const postTaskPriority = (() => {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
return 'user-blocking';
case LowPriority:
case NormalPriority:
return 'user-visible';
case IdlePriority:
return 'background';
default:
return 'user-visible';
}
})();
// `postTaskPriority` can NEVER be reassigned again, because it is a `const`! 🎉
No let. No risk of accidental reassignment. And we even skip all the break
statements! HUGE WIN!
You can also apply the same pattern to if
statements. Here’s a real-world example from Angular:
let kind: DisplayInfoKind;
if (symbol.kind === SymbolKind.Reference) {
kind = DisplayInfoKind.REFERENCE;
} else if (symbol.kind === SymbolKind.Variable) {
kind = DisplayInfoKind.VARIABLE;
} else if (symbol.kind === SymbolKind.LetDeclaration) {
kind = DisplayInfoKind.LET;
} else {
throw new Error(
`AssertionError: unexpected symbol kind ${SymbolKind[(symbol as Symbol).kind]}`,
);
}
With IIFE:
const kind = (() => {
if (symbol.kind === SymbolKind.Reference) {
return DisplayInfoKind.REFERENCE;
}
if (symbol.kind === SymbolKind.Variable) {
return DisplayInfoKind.VARIABLE;
}
if (symbol.kind === SymbolKind.LetDeclaration) {
return DisplayInfoKind.LET;
}
throw new Error(
`AssertionError: unexpected symbol kind ${SymbolKind[(symbol as Symbol).kind]}`,
);
})();
Clean, top-down logic. Guard clauses instead of nested else-ifs. And as a bonus, no need explicitly type kind
as TypeScript infers the type for free!
Use Case 3: Inline Guard Clauses
Ever had to chain multiple ternary operators in your code?
Here's a code snippet from Vue.js:
const defer =
typeof process !== 'undefined' && process.nextTick
? process.nextTick
: typeof Promise !== 'undefined'
? fn => Promise.resolve().then(fn)
: typeof setTimeout !== 'undefined'
? setTimeout
: noop
Multiple chain of ternary operators can become unreadable very quickly. With IIFE:
const defer = (() => {
if (typeof process !== 'undefined' && process.nextTick) {
return process.nextTick;
}
if (typeof Promise !== 'undefined') {
return fn => Promise.resolve().then(fn);
}
if (typeof setTimeout !== 'undefined') {
return setTimeout;
}
return noop;
})();
It is so much cleaner, right?
Each branch is clear, readable, and obvious. You can instantly see there are three conditions and a default fallback to noop
.
Use Case 4: Stateful Functions
Let’s say you need a function that tracks its own state:
let nextCount = 0;
const getNextCount = () => nextCount++;
But nextCount
is exposed to the whole file now! Anyone can modify it and it comes down to trusting developers and your future self not to mess with it!
Now try:
const getNextCount = (() => {
let nextCount = 0;
return () => nextCount++;
})();
Fully encapsulated. No accidental modifications.
Use Case 5: Object Property Initialisation
IIFE can be used to create a more encapsulated object construction pattern.
I came across this piece of code from Angular:
export async function generateMetadata(
path: string,
config: TutorialConfig,
files: FileAndContentRecord,
): Promise<TutorialMetadata> {
const tutorialFiles: FileAndContentRecord = {};
const {dependencies, devDependencies} = JSON.parse(
files['package.json'] as string,
) as PackageJson;
config.openFiles?.forEach((file) => (tutorialFiles[file] = files[file]));
return {
type: config.type,
openFiles: config.openFiles || [],
allFiles: Object.keys(files),
tutorialFiles,
answerFiles: await getAnswerFiles(path, config, files),
hiddenFiles: config.openFiles
? Object.keys(files).filter((filename) => !config.openFiles!.includes(filename))
: [],
dependencies: {
...dependencies,
...devDependencies,
},
};
}
Pretty standard pattern. We do some setup — in this case, computing tutorialFiles
and parsing dependencies
— and then use them in the returned object.
But the setup code can get mixed up really easily and the setup and the usage of the setup are in 2 completely different places!
With IIFE, we can keep each property's setup and usage within its own function:
export async function generateMetadata(
path: string,
config: TutorialConfig,
files: FileAndContentRecord,
): Promise<TutorialMetadata> {
return {
type: config.type,
openFiles: config.openFiles || [],
allFiles: Object.keys(files),
tutorialFiles: (() => {
const tutorialFiles: FileAndContentRecord = {};
config.openFiles?.forEach((file) => (tutorialFiles[file] = files[file]));
return tutorialFiles;
})(),
answerFiles: await getAnswerFiles(path, config, files),
hiddenFiles: config.openFiles
? Object.keys(files).filter(
(filename) => !config.openFiles!.includes(filename),
)
: [],
dependencies: (() => {
const { dependencies, devDependencies } = JSON.parse(
files["package.json"] as string,
) as PackageJson;
return {
...dependencies,
...devDependencies,
};
})(),
};
}
Use Case 6: Try-Catch IIFE
Go check out my previous article to learn more.
But basically:
const user = (() => {
try {
return await getUser();
} catch (err) {
// handle err
return null;
}
})();
Use Case 7: Hierarchies of Abstraction
Sometimes our code can look very flat even when semantically there should be some sort of hierarchies:
const getPosition = (...) => {
const a = ...
const b = ...
const x = getX(a, b);
const c = ...
const d = ...
const y = getY(c, d);
return [x, y];
};
In this example, the hierarchy look something like this:
With IIFE, we can visually represent such hierarchy:
const getPosition = (...) => {
const x = (() => {
const a = ...
const b = ...
return getX(a, b);
})();
const y = (() => {
const c = ...
const d = ...
return getY(c, d);
})();
return [x, y]
};
It is clear, encapsulated, and easy to zoom in and out of the logic without friction.
Final Thoughts
Despite being a pattern born in the early days of JavaScript, IIFE still earns its place in modern development. It’s no longer something we have to use — and that’s precisely what makes it powerful.
When you choose to use IIFE today, you’re doing it for structure, clarity, and intent. You’re declaring, “this bit of logic is self-contained and intentional.” Whether it's to enable async/await
, protect internal state, or express a hierarchy of abstraction, IIFE gives you an extra layer of control over your code.
Sure, it can be noisy — but in the right places, it can be beautiful.
So the next time you reach for a let
just because switch
says so… maybe, just maybe, give IIFE another look.
Let me know what you think — I’d love to hear your thoughts!
I use that async IIFE in React effects all the time, makes things so much cleaner. Ever hit any quirks using IIFE with TypeScript types?