Your AI coding agent will hand you local functions sooner or later. I found use for them while refactoring old code containing methods longer than a full page of scrolling, some leaning on private helpers that existed only to serve the calling methods. Some helpers sat far from the method they served and had names too generic, and were one copy-paste away from being misused.
I hold myself to the SRP when structuring code: a method does a single job. If I can't see the whole method on screen without scrolling, roughly half a screen, it is probably doing more than one job. Long methods need splitting, and local functions and private methods are the tools I reach for.
This isn't a syntax tutorial. It's a design question: when a helper belongs to one method, should it be a private method or a local function? I'll also argue that knowing features like this matters more now that AI writes so much of our code, because you can't properly review a feature you don't know.
What is a local function?
A local function is a method declared inside another method, constructor, property accessor, lambda, or even another local function. Only the code that contains it can see it. Local functions were introduced in C# 7.0, and later versions have steadily added to what they can do. Here is a method that demonstrates a local function in use:
private bool IsSearchMatch(ArticleDto article)
{
if (string.IsNullOrWhiteSpace(_searchText))
return true;
var searchValue = _searchText.Trim();
return Contains(article.Title) || Contains(article.Subject) || Contains(article.Tag.Label);
// local function to check if a string contains the search value, ignoring case
bool Contains(string value) => value.Contains(searchValue, StringComparison.OrdinalIgnoreCase);
}
Contains is called three times with different values, and it uses searchValue from the enclosing method without taking it as a parameter. As a private method it would need searchValue passed on every call, and it would sit in the class for any other member to call, even though it only makes sense inside this one search check.
Three things to notice:
- It looks like a normal method: return type, parameters, block or expression body.
- It can be declared after the
return statement, so the main flow reads first and the helpers sit underneath.
- It can read and modify the enclosing method's variables.
Local functions vs private methods
Both hide a helper from the outside world. The difference is how far the hiding goes.
- Visibility: a local function is visible only to its enclosing method. A private method is visible to the whole class.
- What it tells the reader: a local function says this helper exists for this method alone. A private method says it is part of the class.
- Captures outer variables: a local function can read them directly. A private method needs parameters or fields.
- Reuse by other members: a local function can't be reused elsewhere. A private method can, intended or not.
- Effect on class size: a local function adds nothing to the class. A private method is one more member in the list.
That second point is the whole argument. A private method is private to the class, and in a large or legacy class that still means dozens of members that might call it. A local function says, in the code itself, that nobody else should.
Why use a local function?
1. It limits scope and signals intent
A developer who finds a private method assumes it is fit for reuse. That developer might be you in six months. When a helper encodes a rule that only makes sense in one place (say, skipping one specific customer when ranking loan applicants), the narrowest scope is the honest one.
2. It keeps the logic readable top to bottom
The helper sits right next to the code that uses it. Put the main flow first and the helpers below, and the method reads like a short story followed by footnotes. It also beats a long lambda in the middle of an argument list, because a descriptive name tells the reader what the code is for before they see how it works.
3. It cleans up legacy duplication without cluttering the class
Old code often repeats a block with one value changed. Extracting a private method fixes the duplication but adds yet another vaguely named member, such as BuildThing() or DoSomething(), to a class that already has too many. A local function that takes the varying value as a parameter removes the duplication and leaves the class's member list untouched. This was my entry point.
These three are the reasons that mattered in the refactoring job I described at the start: methods too long to see at once, helpers sitting too far from the code they serve, and duplication with no good place to live. Local functions have other uses beyond cleaning up old code, such as validating arguments eagerly in iterator and async methods, recursive helpers, and allocation-sensitive code, and they are worth learning from the Microsoft documentation. This article sticks to the refactoring case.
When to use which
Use a local function when:
- The helper is used by exactly one method and has no meaning elsewhere
- A lambda would need a name to be understandable
- You're refactoring a long method into named steps and the class is already crowded
Use a private method when:
- A second member needs the helper. Promote it the moment that happens.
- The helper is long or complex enough to deserve its own place in the class's structure
- The logic deserves its own tests. Neither a private method nor a local function can be unit tested directly, so extract it into its own class instead.
Don't:
- Nest five local functions inside a 300-line method. That isn't refactoring, it's relocation.
- Let a local function read and mutate six outer variables. Pass parameters or reconsider the design.
- Use one just because you can. If a private method reads fine, leave it.
Why this still matters in the age of AI
There is a fair question hiding behind an article like this: if a coding agent writes the code, why learn the language features at all?
Because the agent will use them whether you know them or not. Ask a coding agent to refactor a long method and there is a good chance it returns local functions. If you've never seen the feature, you can't tell whether that was a smart choice or an unnecessary one. I make a point of knowing why every line exists in any codebase I work on, whether an AI or a colleague wrote it, so I review every line, and you should too. You own the result, not the agent.
Reviewing code you don't understand is not review. It's approval. Knowing the feature is what turns a skim into a real check:
- Does it capture state it shouldn't? A local function can silently read and modify outer variables. Was that intended?
- Is a local function the right home? A helper used by a second method belongs in a private method. A local function buried in a 300-line method is relocation, not refactoring.
- Is the nesting earning its keep? Coding agents tend to over-engineer. If a private method would read just as well, say so and change it.
This is the same point I keep coming back to: AI hasn't removed the need to understand the craft, it has moved it upstream. The typing is cheaper. The judgment about whether the code in front of you is correct, idiomatic and in the right place is not, and that judgment only comes from knowing what the language offers and when each option fits.
I also have a firm stand on readability. A coding agent will happily generate code that works but is dense and cryptic: chained one-liners, clever tricks, helpers nested inside helpers. I don't accept that just because it compiles. Code is read far more often than it is written, and the next reader may be a colleague, or me in six months, without the prompt that produced it. If I can't understand a method at a glance, it fails review, however clever it is. I ask the agent for something simpler, or I rewrite it myself. Readable code is the goal, and that is the same reason I keep methods short and give each one a single job.
The takeaway
Local functions are not a flashy feature, and that's why they're worth knowing. They express one simple idea in code: this helper belongs to this method, and nowhere else. Where that is true, use one. Where it isn't, a private method is the right tool.
As always in engineering, the syntax is the easy part. The judgment about where a piece of logic really belongs is the craft. If you have a method full of single-use helpers in your own code, move one into a local function and see how the method reads afterward.