Back to JavaScript
JavaScript
easy
mid

Can you explain closures, hoisting, the event loop, and currying in JavaScript?

Four core JS concepts: closures (a function + its remembered lexical scope), the event loop (the scheduler that runs queued async callbacks on the single thread), hoisting (declarations are processed before execution — var/function hoisted, let/const in the temporal dead zone), and currying (transforming f(a,b,c) into f(a)(b)(c) via closures).

6 min read·~10 min to think through

A rapid-fire fundamentals question covering four staples. Here's a crisp explanation of each.

Closure

A closure is a function together with the lexical scope it was defined in. The function keeps access to those outer variables even after the outer function has returned.

js
function makeAdder(x) {
  return (y) => x + y;   // closes over x
}
const add5 = makeAdder(5);
add5(3); // 8

Uses: data privacy, function factories, memoization, event handlers, React hooks.

Event loop

JS is single-threaded. The event loop is the scheduler: async work (timers, fetch, events) is handled by Web APIs; their callbacks queue up; when the call stack is empty, the loop drains all microtasks (Promises) then runs one macrotask (timers/events), repeating.

js
console.log(1);
setTimeout(() => console.log(2));         // macrotask
Promise.resolve().then(() => console.log(3)); // microtask
console.log(4);
// 1, 4, 3, 2

Hoisting

During the creation phase of an execution context, declarations are registered before any code runs:

  • var → hoisted, initialized to undefined.
  • function declarations → fully hoisted (callable before their line).
  • let / const → hoisted but uninitialized — referencing them before declaration throws (the Temporal Dead Zone).
js
console.log(a); // undefined  (var hoisted)
console.log(b); // ReferenceError (let in TDZ)
var a = 1;
let b = 2;

Currying

Currying transforms a function of N arguments into N nested functions of one argument each — built on closures.

js
const curry = (fn) => {
  return function curried(...args) {
    return args.length >= fn.length
      ? fn(...args)
      : (...more) => curried(...args, ...more);
  };
};
const sum = curry((a, b, c) => a + b + c);
sum(1)(2)(3);   // 6
sum(1, 2)(3);   // 6

Uses: partial application, reusable specialized functions, point-free composition.

Senior framing

The thread connecting them: closures are the foundation — currying is closures, hoisting determines when the bindings closures capture exist, and the event loop is why a deferred callback still needs a closure to reach its data. Knowing each individually is junior; articulating that closures underpin three of the four is senior.

Follow-up questions

  • How is currying implemented using closures?
  • What's the Temporal Dead Zone?
  • Why are function declarations hoisted but function expressions aren't?

Common mistakes

  • Saying let/const aren't hoisted — they are, but uninitialized (TDZ).
  • Confusing currying with partial application.
  • Thinking the event loop makes JS multi-threaded.

Edge cases

  • Function declarations hoist above var declarations of the same name.
  • Currying with variadic functions needs an explicit arity or terminator.

Real-world examples

  • Closures in hooks, currying in functional libraries (Ramda, lodash/fp), hoisting bugs with var.

Related questions