ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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:

Program.cs
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.

Program.cs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.