C# local functions
last modified October 16, 2026
In this article we show how to use local functions in C#.
A local function is a method declared inside another member, such as a method, a constructor, a property accessor, or a lambda expression. It can be called only from the containing member. Local functions make it clear that a helper is used in one place only, and they keep the containing class free of private helper methods.
Local functions were introduced in C# 7. Functions declared in top-level statements are local functions too.
C# local function example
In the first example, we define a local function inside a method.
var report = new Report();
report.Print([12.5m, 8.25m, 30m]);
class Report
{
public void Print(decimal[] prices)
{
foreach (var price in prices)
{
Console.WriteLine(Format(price));
}
Console.WriteLine(Format(prices.Sum()));
string Format(decimal value) => $"{value,10:F2} EUR";
}
}
The Format function is declared inside the Print
method.
string Format(decimal value) => $"{value,10:F2} EUR";
A local function can be declared anywhere in the containing member; even after the statements that call it. It has no access modifier, because it is always private to the member.
$ dotnet run
12.50 EUR
8.25 EUR
30.00 EUR
50.75 EUR
C# capturing variables
A local function can access the local variables and parameters of the containing member. Such a function is a closure.
var words = new[] { "sky", "falcon", "cloud", "ocean", "by", "forest" };
int minLength = 5;
int count = 0;
foreach (var word in words)
{
if (IsLong(word))
{
count++;
}
}
Console.WriteLine($"{count} words have at least {minLength} characters");
bool IsLong(string word) => word.Length >= minLength;
The IsLong function reads the minLength variable of
the enclosing scope.
$ dotnet run 4 words have at least 5 characters
Since the value of a captured variable is read when the function is called, a change of the variable is visible inside the function.
C# static local functions
A local function declared with the static modifier cannot capture
local variables or the this reference. All data must be passed as
parameters. This makes the dependencies of the function explicit and prevents
accidental captures.
int[] vals = [3, -2, 7, 0, -9, 4];
int limit = 3;
Console.WriteLine(CountAbove(vals, limit));
static int CountAbove(int[] data, int threshold)
{
int n = 0;
foreach (var e in data)
{
if (e > threshold) n++;
}
return n;
}
The CountAbove function receives all its inputs through
parameters.
$ dotnet run 2
If a static local function referenced the limit variable directly,
the compiler would report the CS8421 error: a static local function cannot
contain a reference to a local variable.
C# recursive local functions
A local function can call itself. This is useful for recursive algorithms that need a helper with extra parameters.
var tree = new Node("root",
[
new Node("src", [new Node("Program.cs"), new Node("Utils.cs")]),
new Node("docs", [new Node("README.md")]),
new Node("app.csproj")
]);
PrintTree(tree);
void PrintTree(Node root)
{
Print(root, 0);
static void Print(Node node, int level)
{
Console.WriteLine($"{new string(' ', level * 2)}{node.Name}");
foreach (var child in node.Children)
{
Print(child, level + 1);
}
}
}
record Node(string Name, Node[] Children)
{
public Node(string name) : this(name, []) { }
}
The public-facing PrintTree function takes only the root node. The
nested Print function carries the indentation level.
$ dotnet run
root
src
Program.cs
Utils.cs
docs
README.md
app.csproj
C# argument validation in iterators
An iterator method, which uses yield return, does not run until
the result is enumerated. Therefore, argument checks inside an iterator are
deferred. The common solution is to validate the arguments in a regular method
and put the iterator in a local function.
IEnumerable<int> nums;
try
{
nums = Range(1, -5);
Console.WriteLine("Range created");
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Error: {e.ParamName}");
}
foreach (var n in Range(1, 5))
{
Console.Write($"{n} ");
}
Console.WriteLine();
static IEnumerable<int> Range(int start, int count)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
return Iterate();
IEnumerable<int> Iterate()
{
for (int i = 0; i < count; i++)
{
yield return start + i;
}
}
}
The Range function is not an iterator; it checks the argument
immediately and returns the result of the Iterate local
iterator.
$ dotnet run Error: count 1 2 3 4 5
The exception is thrown when Range is called, not later when the
sequence is enumerated. The same technique is used for async
methods.
C# local functions vs lambda expressions
Local functions and lambda expressions are similar, but local functions have several advantages:
- they can be generic
- they can be recursive without tricks
- they can be iterators and use
yield return - they can be called before their declaration
- they usually do not allocate a delegate object
Console.WriteLine(Fact(10));
Console.WriteLine(Max(3, 9));
Console.WriteLine(Max("ocean", "cloud"));
Func<int, long>? fact = null;
fact = n => n <= 1 ? 1 : n * fact!(n - 1);
Console.WriteLine(fact(10));
static long Fact(int n) => n <= 1 ? 1 : n * Fact(n - 1);
static T Max<T>(T a, T b) where T : IComparable<T> =>
a.CompareTo(b) >= 0 ? a : b;
The recursive lambda must be declared first and assigned later, because it
refers to its own variable. The local function is simpler. The generic
Max function cannot be written as a lambda expression.
$ dotnet run 3628800 9 ocean 3628800
Lambda expressions are still the natural choice for short inline functions passed as arguments, such as LINQ predicates.
C# local functions cannot be overloaded
Local functions in the same scope must have unique names; they cannot be overloaded. This also applies to functions in top-level statements.
Console.WriteLine(Area(3));
Console.WriteLine(Area(3, 4));
double Area(double width, double height = -1) =>
height < 0 ? width * width : width * height;
Instead of two overloads, we use an optional parameter. If we declared two
Area functions, the compiler would report the CS0128 error: a
local variable or function with the same name is already defined in this
scope. If overloads are needed, we use methods of a class.
$ dotnet run 9 12
Source
Local functions - C# programming guide
In this article we have worked with local functions in C#.
Author
List all C# tutorials.