Best of C# 14 - field keyword
The new 'field' keyword is in C# 14. When can you use it and when might you not.
C# 14 shipped back in November last year along with .NET 10. I’ve already given a presentation a number of times on my highlights of .NET 10, which included three features of C# 14 that I think are particularly useful.

The first I’m going to focus on is the new field keyword.
The problem this solves is where you’d really like to avoid having to declare a backing field, but prior to C# 14 as soon as you wanted to include any kind of logic in a getter or setter, then you couldn’t use auto properties, but instead you needed to provide a backing field and implement the getter and setter. eg.
private string _msg;
public string Message
{
get => _msg;
set => _msg = value ?? throw new ArgumentNullException(nameof(value));
}
Because we’re adding validation to the setter, that forced us to introduce the _msg field. And if you can be disciplined that’s fine, but the trap is there’s nothing to stop other code in the same class from also referencing that, even if it shouldn’t. The compiler won’t stop you.
To help with scenarios like this, and to reduce the amount of code you need to write, C# 14 introduces the field keyword. It’s a way to reference the compiler-generated backing field, but only within the property’s getter and setter. Our code now becomes:
public string Message
{
get;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
Our line count is one less, plus we’re now not exposing the backing field outside of the property.
Under the hood
When the C# compiler turns the original code into IL (Intermediate Language), it creates something like this:
.field private string _msg
That’s how IL represents a regular field.
And if you search the IL generated by code that uses the field keyword, you’ll still see a .field entry, but the name is a bit different:
.field private string '<Message2>k__BackingField'
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState)
= (01 00 00 00 00 00 00 00 )
<Message2>k__BackingField is not a valid C# identifier, so there’s no chance of being able to reference this field by name elsewhere in your C# code. That DebuggerBrowsableAttribute also means that your debugger will choose to not show the field.
But because the compiler knows the name it has assigned to the field, it generates the correct IL for you when you use the field keyword.
Here’s the IL generated for the property setter:
IL_0000: ldarg.0 // this
IL_0001: ldarg.1 // 'value'
IL_0002: dup
IL_0003: brtrue.s IL_0011
IL_0005: pop
IL_0006: ldstr "value"
IL_000b: newobj instance void [System.Runtime]System.ArgumentNullException::.ctor(string)
IL_0010: throw
IL_0011: stfld string Stuff::'<Message>k__BackingField'
IL_0016: ret
The stfld instruction is saving the current value from the stack into the the compiler-generated backing field.
Reasons for not using field
- If you have legitimate reasons for accessing the backing field elsewhere in the class (such as multiple properties that reference or share the same backing field).
- Thread safety using
volatile,Interlockedorlockpatterns. - Lazy initialisation/caching patterns
- Serialization or reflection that makes assumptions about field names