LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 819 min read

Closures & Scope

Understand lexical scoping and closures in Lua — how a function can capture and remember variables from the scope it was created in.

Introduction

Closures are one of Lua's most powerful features, and one of the things that changed most significantly in the Lua 5.0 redesign covered in lesson 2. A closure is simply a function that "remembers" the variables from the scope it was created in, even after that scope has technically finished executing.

What You Will Learn
  • How lexical (block-based) scope works in Lua.
  • What a closure is and what it means for a function to capture a variable.
  • The classic counter-generator pattern, and why it only works because of closures.
  • How closures enable private, encapsulated state without classes.

Lexical Scope

A local variable is only visible inside the block (do...end, a function, a loop, an if) where it was declared, and any blocks nested inside it — this is lexical (or "static") scoping, determined by where code is written, not by the order functions happen to run.

local outer = "I'm outside"
do
local inner = "I'm inside"
print(outer) -- visible: outer block variables are visible to inner blocks
print(inner)
end
print(outer) -- still visible
-- print(inner) -- would error: inner is out of scope here
Output

Click Run to see what this code prints.

What a Closure Actually Is

When a function is defined inside another function (or block) and references a local variable from that outer scope, Lua keeps that variable alive for as long as the inner function might still need it — even after the outer function has already returned. The inner function, plus the variables it captured, together form a closure.

local function makeGreeter(greeting)
return function(name)
return greeting .. ", " .. name .. "!"
end
end
local sayHello = makeGreeter("Hello")
local sayHowdy = makeGreeter("Howdy")
print(sayHello("Ravi"))
print(sayHowdy("Ravi"))
Output

Click Run to see what this code prints.

Each Closure Gets Its Own Captured Copy

sayHello and sayHowdy each captured their own greeting variable at the moment makeGreeter ran — makeGreeter("Hello") returned normally, but Lua kept its local greeting alive specifically because the returned function still references it.

The Classic Counter Example

The single most common closure example, in nearly every language that has them, is a counter generator — a function that returns a new function, where each returned function has its own private, persistent count.

local function makeCounter()
local count = 0
return function()
count = count + 1
return count
end
end
local counterA = makeCounter()
local counterB = makeCounter()
print(counterA()) -- 1
print(counterA()) -- 2
print(counterB()) -- 1 (its own independent count)
print(counterA()) -- 3
Output

Click Run to see what this code prints.

Closures for Encapsulation

Because a captured local variable is invisible from outside the closure, closures give you real private state with no class syntax at all — count in the example above cannot be read or modified from outside makeCounter except through the returned function itself. This same pattern is exactly how Lua's module system (lesson 11) hides internal implementation details.

Common Mistakes

Avoid These Mistakes
  • Assuming a closure copies a variable's value at creation time — it actually captures the variable itself, so later changes to it are visible inside the closure.
  • Creating closures inside a loop that all accidentally capture the same loop variable, expecting each to have its own snapshot — check Lua's scoping rules for your specific loop type carefully.
  • Confusing "the function has access to outer variables" (true of any nested function) with "a closure" specifically — a closure is what you get once that outer function has returned and the variable is still kept alive.

Best Practices

  • Use closures deliberately for private state (like the counter example) instead of leaking implementation details as global or exposed table fields.
  • Keep closures small and focused — a closure capturing many variables can become hard to reason about.
  • Reach for the module pattern (lesson 11) once encapsulated state needs to be shared and organized across multiple functions.

Frequently Asked Questions

Technically, yes — every Lua function is a closure over its enclosing environment, even if it doesn't capture any locals. The term is just most useful (and most commonly used) when a function genuinely captures and reuses outer variables.

No — closures exist in JavaScript, Python, Rust, and many other languages. The underlying concept (a function remembering its defining scope) transfers directly, even though the exact syntax differs.

It's Lua's specific term for the outer-scope variables a closure captures — you'll see the term "upvalue" in Lua's documentation and debugging tools referring to exactly this mechanism.

Key Takeaways

  • Lua uses lexical scoping — a variable is visible in the block it's declared in and any nested blocks.
  • A closure is a function that captures and keeps alive variables from its enclosing scope, even after that scope has finished.
  • Each call to a function that returns a closure creates a fresh, independent set of captured variables.
  • Closures enable private state and encapsulation without any class syntax.

Summary

Closures are what let Lua build patterns other languages need classes for, using nothing more than functions and tables. Next, you'll combine tables and functions one step further with metatables — the mechanism behind Lua's object-oriented programming and operator overloading.

Next Lesson →

Metatables & Object-Oriented Lua