C# 14 Null-Conditional Assignment: ?. on the Left Side
The null-conditional operator (lesson 051) has always been able to answer "what's in there, if anything." C# 14 finally lets it also answer "put this in there, if there's anywhere to put it."
Lesson 051 taught you ?. as the null-safe way to read a member: var city = customer?.Address?.City; quietly evaluates to null the instant any link in that chain is null, instead of throwing a NullReferenceException. It's one of the most useful pieces of syntax in the language — but only ever on the read side. Try to use it on the write side, as an assignment target, and before C# 14 that was a flat compile error: customer?.Address = newAddress; simply wasn't legal C#. You had to fall back to the exact defensive if block the operator exists to eliminate everywhere else.
In this lesson, you'll learn that C# 14 lifts that restriction: ?. can now appear on the left side of an assignment, meaning "assign this — but only if the thing on the left isn't null; otherwise, skip the assignment entirely and do nothing."
What Is It?
The Simple Explanation
Null-conditional assignment lets you write customer?.Address = newAddress; directly. If customer is not null, the assignment happens exactly as you'd expect. If customer is null, the entire right-hand side is never evaluated, no assignment happens, and — critically — nothing throws. Execution simply moves on to the next statement.
The Technical Definition
C# 14 extends the null-conditional operator (?. and, correspondingly, ?[...] for indexers) to be legal as the target of a simple assignment expression. When the receiver evaluates to null, the entire assignment expression short-circuits: the right-hand side is never evaluated at all — an important detail if that expression has side effects — and the statement completes with no exception and no assignment performed. When the receiver is non-null, the assignment proceeds exactly as an ordinary, unconditional assignment would.
Before C# 14
if (customer != null) customer.Address = newAddress;- Two statements, or a full
ifblock, for one conditional write customer?.Address = newAddress;was a compile error- Read side (
?.) and write side had inconsistent capability
With C# 14
customer?.Address = newAddress;— one line, same meaning- Null check and conditional write fused into one expression
- Right-hand side never evaluated when the receiver is null
- Read and write sides of
?.are now symmetric
Why Does It Exist?
The Problem — An Asymmetric Operator
The null-conditional operator's whole reason for existing, since lesson 051, has been to collapse "check for null, then act" into one expression — for reads. But the exact same pattern on a write was left completely unaddressed: a conditional assignment still required the full, explicit defensive-if boilerplate the operator was invented to eliminate everywhere else. Developers routinely reached for ?. out of habit when writing to a possibly-null reference, hit a compile error, and had to unwind back to the older, more verbose pattern — an inconsistency that stood out precisely because the operator worked so well everywhere else.
The Solution — Make the Operator Symmetric
C# 14 closes the gap by allowing the exact same ?. syntax to appear on the left of an assignment, with the exact same "null means skip, don't throw" semantics you already know from reads. Nothing new needs to be learned conceptually — it's the same operator, doing the same kind of short-circuiting, in the one remaining place it hadn't been allowed to.
Big Picture
var x = obj?.Prop; — null-safe since C# 6obj?.Prop = value; — null-safe since C# 14How It Works
- The expression before
?.is evaluated exactly once.
- If yes: stop here. The right-hand side (
newAddress) is never evaluated — this matters if it were, say, a method call with side effects. No assignment happens. No exception is thrown. The statement is done.
newAddressis evaluated normally, exactly as it would be in any ordinary assignment.
customer.Addressis set to the evaluated right-hand side value — identical to whatcustomer.Address = newAddress;would have done without the?.at all.
Simple Example
public class Address { public string City { get; set; } = ""; }
public class Customer { public Address? Address { get; set; } }
Customer? customer = FindCustomer(id); // might return null
// Before C# 14 — the defensive if block:
if (customer != null)
{
customer.Address = new Address { City = "Seattle" };
}
// C# 14 — the exact same behavior, one line:
customer?.Address = new Address { City = "Seattle" };
// If customer is null here, nothing happens — no exception, no assignment,
// and new Address { City = "Seattle" } is never even constructed.Code → Meaning → Result: Both blocks behave identically at runtime. The C# 14 version simply says the same thing in one expression instead of three lines, and — notably — skips constructing the right-hand side entirely when customer is null, since that expression is never reached.
Real-World Example
Consider an event handler pattern you've likely written many times: an optional callback field that a caller may or may not have set. OnStatusChanged?.Invoke(newStatus); is the classic null-conditional read-side pattern for safely raising an event that might have no subscribers. Null-conditional assignment now gives you the same safety on the configuration side: a UI component with an optional, injectable logger — _diagnostics?.LastError = ex.Message; — records the error only if a diagnostics object was actually supplied, without a wrapping if block cluttering what is, conceptually, a single, simple "record this if there's somewhere to record it" operation. Multiply that pattern across dozens of optional dependencies in a large application, and the reduction in defensive-null-check boilerplate adds up.
Analogy
Reading with ?. is like checking a mailbox that might not be there: if it exists, you take out the mail; if it doesn't, you walk away empty-handed, without incident. Null-conditional assignment is the exact same idea run in reverse — you're trying to drop a letter into a mailbox that might not exist. If the mailbox is there, the letter goes in, exactly as normal. If it isn't, you simply don't drop the letter — you don't stand there confused, and you certainly don't set off an alarm. The letter you were holding (the right-hand side) doesn't even need to be written yet if there's no mailbox to receive it, which is exactly why the right-hand side is never evaluated when the receiver is null.
Under the Hood
- Short-circuits before the right-hand side. The compiler generates code that checks the receiver for null before evaluating the value being assigned — if the receiver is null, the right-hand side expression is skipped entirely, not merely its assignment. This matters whenever the right-hand side has observable side effects, such as a method call or a constructor with logging inside it.
- Works for simple assignment only. The feature covers ordinary = assignment through a null-conditional target; it doesn't change anything about compound assignment operators or about the read-side behavior you already knew from lesson 051.
- Same underlying null check either way. Under the hood, this compiles to essentially the same conditional check a hand-written
ifwould produce — there's no meaningful runtime cost difference from the defensive pattern it replaces, just less code to read and maintain.
Common Confusion
1. "The right-hand side always runs, only the assignment is skipped" — no, the whole thing is skipped
It's tempting to assume the value being assigned is always computed and only the final "store it" step is conditional. That's incorrect: when the receiver is null, C# never evaluates the right-hand side at all. If that expression calls a method with a side effect — logging, incrementing a counter, mutating something else — that side effect simply doesn't happen when the receiver is null. This is exactly consistent with how ?. has always short-circuited on the read side, but it's worth stating explicitly, since an assignment's right side is easy to assume "always runs."
2. "This throws if the receiver is null, just like a plain assignment would" — no, it's specifically designed not to
An ordinary customer.Address = newAddress; throws a NullReferenceException the instant customer is null. The entire point of null-conditional assignment is the opposite guarantee: a null receiver means the statement quietly does nothing and execution simply continues — no exception at all. If you actually want an exception when the receiver turns out to be null, this is the wrong tool; keep the ordinary, unconditional assignment for that case.
Common Mistakes
Mistake 1 — Using it where a null receiver should actually be an error
Reaching for customer?.Address = newAddress; in a code path where customer being null genuinely represents a bug — silently swallowing what should have been a loud, early failure, and letting the program continue in a state its author never intended.
Use null-conditional assignment specifically where "there's genuinely nothing to do" is a legitimate, expected outcome — an optional dependency, an event with no subscribers. Where a null value is actually invalid, keep an explicit check (or lesson 041's exception-throwing patterns) so the bug surfaces immediately instead of being silently absorbed.
Mistake 2 — Assuming the right-hand side always executes
Writing logger?.LastMessage = ComputeExpensiveDiagnostic(); and assuming ComputeExpensiveDiagnostic() always runs regardless of whether logger is null — then being surprised when a side effect inside it (like a counter increment) doesn't happen in some code paths.
Remember that the right-hand side is skipped entirely when the receiver is null. If a right-hand-side expression's side effects must always happen regardless of the receiver, compute it in a separate statement first, then assign it unconditionally.
When Should I Use It?
- Whenever you'd otherwise write a single-statement
if (x != null) x.Member = value;block — this is the exact pattern the feature is meant to collapse. - For genuinely optional targets — an injected dependency that might not be configured, an object that legitimately may or may not exist in this code path.
- Not where a null receiver represents an actual bug that should be surfaced loudly — keep explicit validation and exceptions there instead.
- Be mindful when the right-hand side has side effects you need to guarantee happen — compute it separately first if so.
Mental Model
obj?.Prop → null if obj is null, no exception.Write side (C# 14):
obj?.Prop = value; → nothing happens if obj is null, no exception.Either way: null means "skip," not "crash" — and on the write side, the value being assigned isn't even computed when skipped.
Remember:
if (x != null) x.Member = value; and x?.Member = value; are the same thing — one is just shorter.
Key Takeaway
- C# 14 allows the null-conditional operator (
?., from lesson 051) to appear as an assignment target:customer?.Address = newAddress;now compiles. - If the receiver is null, the assignment — and the entire right-hand side expression — is skipped; nothing throws.
- If the receiver is non-null, the assignment proceeds exactly as an ordinary, unconditional assignment would.
- This is a direct reduction of the
if (x != null) x.Member = value;boilerplate pattern into ordinary null-conditional shorthand — the write-side counterpart to a read-side pattern you've already used since lesson 051. - Reach for it where a null receiver is a legitimate, expected "nothing to do" case — not where a null value should actually be treated as a bug.
Check Your Understanding
Let's confirm the short-circuiting behavior — including the right-hand-side subtlety — landed clearly.
1. What happens when customer?.Address = newAddress; executes and customer is null?
Show answer
Correct: B
Why B is correct: This is the entire point of the feature — a null receiver means the assignment is skipped safely, mirroring the read-side ?. behavior from lesson 051 rather than throwing.
Why A is incorrect: Throwing on a null receiver is exactly the ordinary-assignment behavior this feature exists to avoid; the whole motivation was skipping the exception.
Why C is incorrect: Null-conditional assignment never creates or substitutes a new instance for a null receiver — it simply does nothing when the receiver is null.
Why D is incorrect: This is exactly the syntax C# 14 makes legal — it compiles cleanly, which is the entire subject of this lesson.
Reinforcement: Null-conditional assignment always means "null receiver → skip, don't throw."
2. Given logger?.LastMessage = BuildMessage(); where BuildMessage() has an observable side effect (it increments a counter), and logger is null at runtime — does BuildMessage() get called?
Show answer
Correct: B
Why B is correct: As "Common Confusion" #1 and "Under the Hood" both explain, the short-circuit happens before the right-hand side is evaluated at all — a null receiver means BuildMessage() is never called, and its side effect never occurs.
Why A is incorrect: This is precisely the misconception the lesson calls out — evaluation, not just assignment, is skipped when the receiver is null.
Why C is incorrect: The method call itself doesn't happen at all in this case — there's no return value to discard, because it's never invoked.
Why D is incorrect: Whether a method is asynchronous has no bearing on this short-circuiting rule — the behavior is the same either way.
Reinforcement: A null receiver skips the entire right-hand side expression, not just the final store.
3. Which existing C# pattern does null-conditional assignment most directly replace, according to this lesson?
Show answer
Correct: B
Why B is correct: This is the exact defensive boilerplate pattern the "Why Does It Exist?" and "Simple Example" sections both identify as what null-conditional assignment collapses into one line.
Why A is incorrect: The null-coalescing assignment operator (??=) assigns a fallback to a possibly-null variable itself — a different operator solving a different problem, not this feature.
Why C is incorrect: The null-forgiving operator (!) tells the compiler to suppress a nullable warning while still risking a real runtime exception if the value actually is null — the opposite of null-conditional assignment's safe skip.
Why D is incorrect: Wrapping an assignment in a try/catch to suppress a NullReferenceException is a heavier, less idiomatic pattern than the simple if check this feature actually targets.
Reinforcement: Null-conditional assignment is a direct, one-line replacement for the single-statement defensive-if pattern.
The null-conditional operator you learned in lesson 051 is now fully symmetric — safe on reads and safe on writes. Lesson 327 continues the Part XII tour with enhanced span conversions, and more C# 14 additions follow right after.