Last Updated: May 17, 2026
Most programs do different things depending on the data they're handed. A cart with one item is checked out differently from an empty one. A premium customer sees a different price from a guest. The if statement is how you express that branching in C#, and if/else if/else is how you handle the cases where two outcomes aren't enough. This chapter covers the syntax, the rules C# enforces around conditions, the operators that pair with if (ternary, null-coalescing), and the small mistakes that cost beginners hours of debugging.
if StatementAn if runs a block of code when a condition is true and skips it otherwise. The shape is fixed: the if keyword, a parenthesised condition, and a block in braces.
The condition itemsInCart > 0 produces a bool. If it's true, the block runs. If it's false, the block is skipped and execution continues at the next statement.
The braces are technically optional when the body is a single statement, but C# style guides (and Microsoft's own coding conventions) tell you to use them every time. Brace-less bodies are the source of one of the oldest C-family bugs: someone adds a second line, forgets the braces aren't there, and the second line runs unconditionally.
The indentation suggests both lines belong to the if, but only the first one does. Always use braces.
boolC# is strict about what goes inside the parentheses. The condition must be a bool (or something the compiler can resolve to a bool like a comparison or a bool-returning method). You can't pass an int and expect zero to mean "false" the way some other C-family languages allow.
The compiler reports CS0029: Cannot implicitly convert type 'int' to 'bool'. This catches a whole class of bugs at compile time. If you want to test "is there any stock," write what you mean: if (stock > 0) or if (stock != 0).
The same rule applies to reference types. You can't drop a string or a custom object into an if and have C# treat null as false.
Write the null check explicitly:
The benefit is clarity. When you read the code, the intent is right there: "if the coupon is not null, apply it." Nothing is hiding behind a type conversion.
if/else for Two OutcomesWhen you need to do one thing if a condition holds and a different thing otherwise, use else. The else block runs whenever the if condition is false.
Exactly one of the two blocks runs. There's no overlap, no fall-through. Once the if decides which branch wins, the other is skipped and execution continues after the else block.
A subtle point: the else belongs to the closest unmatched if. That becomes important when you nest them, which we'll look at shortly.
else if Ladders for Multiple CasesWhen you have more than two outcomes, chain else if clauses together. Each condition is checked in order. The first one that's true runs its block, and the rest are skipped.
A common e-commerce use is tiered discounts: the bigger the cart, the bigger the discount. Spend over $200 and you get 20% off, over $100 gets you 10%, over $50 gets you 5%, anything else pays full price.
The order matters. The check for >= 200m has to come before >= 100m, otherwise a $300 cart would match the >= 100m branch first and never reach the 20% tier. Always list ladder conditions from most-restrictive to least-restrictive when ranges overlap.
The trailing else (without a condition) catches anything that didn't match an earlier branch. It's optional, but including it makes the intent explicit and avoids forgetting the "default" case.
Here's the same decision drawn as a flowchart:
The runtime walks down the diagram one decision at a time. As soon as a check returns yes, the matching branch runs and everything below is ignored. The flow always ends at one of the four outcomes, never two.
if StatementsYou can put an if inside another if. The inner one is only reached when the outer condition is true, which is exactly what you want when one check only makes sense after another has passed.
The structure mirrors the way the conditions stack: you can't check the cart until the user is logged in, and you can't check the total until there are items.
Deep nesting is a smell. Three levels and you're already approaching the limit of what someone can hold in their head. The cure is guard clauses: handle the failure cases up front with early exits, then write the happy path at the top level.
Same logic, no pyramid. Each guard reads like a sentence. The happy path is the last line, unindented, and obvious.
&& and || are short-circuit operators. Inside an if condition, they evaluate the right side only when needed. With &&, a false left side ends the evaluation. With ||, a true left side does.
This isn't just an optimisation. It's what makes null-guarding work.
The left side, couponCode != null, is false. && stops there. The right side, couponCode.Length >= 5, is never evaluated, so the null reference never throws. Reverse the order and the program crashes:
The general rule: when a condition has a guard (a null check, a bounds check, a "this exists" check), put the guard first. C# will protect the rest of the expression for you.
|| works the same way, useful when any one of several reasons should short-circuit to success.
As soon as one of the three checks returns true, the rest are skipped. If you put the most likely-true condition first, the average evaluation cost drops.
?:?: is the only ternary operator in C#. It picks one of two values based on a condition. The form is condition ? valueIfTrue : valueIfFalse. It's useful when you want a value, not a side effect, and a full if/else would be heavier than the situation deserves.
The same logic with if/else takes five lines and a temporary variable that has to be declared first. The ternary collapses it to one.
Ternaries work inside larger expressions, which is something if statements can't do. You can use them in string interpolation, method arguments, or return statements directly.
Note the parentheses around the ternary inside the interpolation. Without them, the compiler reads the ? as the start of a format specifier and complains. When in doubt, parenthesise the whole ternary.
Two cases? Ternary is fine. Three or more? Switch to if/else if or, better, a switch expression. Nested ternaries are technically legal but actively hostile to anyone reading the code.
The reader has to mentally parse three nested ternaries to find which branch fires. The same logic with an if/else if ladder is far easier to scan.
Reserve the ternary for binary either-or choices where both branches are short.
?? and ??=: Null-Coalescing in Conditional FlowsWhen the choice you're making is "use this value if it exists, otherwise fall back," reaching for if is overkill. C# has two compact operators for the pattern.
?? returns the left operand if it's non-null, otherwise it returns the right operand. It's the cleanest way to apply a default to a nullable value.
??= (C# 8) is the assignment version. x ??= y assigns y to x only when x is currently null. It's perfect for lazy initialisation of an optional field.
Compared to the equivalent if:
The ??= form is one line, communicates intent clearly, and doesn't tempt anyone into adding more code to the if block later.
These operators replace a lot of the small null checks that beginners write with if. Anywhere you'd say "if x is null, use y instead," prefer ??. Anywhere you'd say "if x is null, set it to y," prefer ??=.
A handful of if patterns show up over and over in real code. Recognising them by name makes them easier to write cleanly.
A guard clause is an early return (or throw) that handles a failure case before the main logic runs. The benefit is that the rest of the method assumes the inputs are valid, so it doesn't need defensive checks scattered throughout.
Each guard handles one edge case in one line. The body of the method, at the bottom, contains the normal flow. Compare that to the nested-if version, which would be three levels deep and harder to follow.
Validation is similar but usually produces an error message or throws an exception instead of returning silently.
Same shape as guard clauses. Each rule lives in its own if, separated from the rest. Adding a new validation later is a matter of inserting one more block at the top.
A range check verifies that a number falls between two bounds. The C# idiom for it is lower <= value && value <= upper.
A common beginner trap is writing 1 <= rating <= 5, copying the form from math notation. That's a compile error in C#. Each <= takes two operands, and chaining them this way produces nonsense like "compare a bool against an int." Break it into two comparisons joined with &&.
Since C# 9, pattern matching gives you a cleaner alternative: rating is >= 1 and <= 5.
A few specific traps catch nearly every C# learner at least once. Knowing about them in advance is enough to dodge them.
= vs === is assignment. == is the equality test. In a condition, you almost always want ==.
In C, this kind of typo silently compiles and changes the value of stock to 0 before evaluating the result. C# saves you: the condition has to be a bool, and stock = 0 produces an int, so the compiler refuses. You get CS0029: Cannot implicitly convert type 'int' to 'bool'.
The one case where it does compile and bite is with bool variables:
This assigns true to isPaid and then uses true as the condition, so the branch runs and isPaid is now true for the rest of the method. The fix is if (isPaid == true), or, even cleaner, if (isPaid).
When you nest if statements without braces, the else binds to the closest if. If your indentation suggests otherwise, the indentation is lying.
The indent suggests the else matches the outer if. It doesn't. C# binds the else to the inner if (cartTotal >= 50m), so when isMember is false, neither branch runs at all and nothing is printed.
The fix is always the same: use braces.
Now the structure is unambiguous and the else clearly pairs with the outer if.
== vs EqualsFor most reference types, == compares references (whether two variables point at the same object). string is the exception: the language overloads == so it compares the actual character sequence. So "shipped" == "shipped" returns true as you'd expect.
Where it gets tricky is when case matters. == and the default Equals are both case-sensitive. "Shipped" and "shipped" are not equal.
If the input is user-entered or comes from an external system where casing isn't guaranteed, use string.Equals with an explicit StringComparison value. OrdinalIgnoreCase is the right pick for non-localised identifiers like status codes, country codes, or product IDs.
Cost: string.Equals(a, b, StringComparison.OrdinalIgnoreCase) is slightly slower than == because it does a character-by-character compare with case folding. The difference is negligible outside of very tight loops, and the safety from case bugs is usually worth it.
One more thing: never use == to compare a string against null if you've also overridden == in your own types. For built-in string, s == null works exactly as expected. But the safest, universal form for any reference type is s is null, which always checks for the literal null reference and ignores any custom == overloads.
This style is the modern recommendation. Microsoft's own analysers will suggest it when they see s == null in nullable-enabled code.
A small program that uses everything from this chapter to decide what a checkout page should show:
Guard clauses up top, then an else if ladder for the discount tier, then a ternary for shipping, then ?? for the optional promo code. Each construct earns its place by being the cleanest way to express one specific decision.
if runs a block when its condition is true. The condition must be a bool; C# rejects implicit conversions from int, string, or other types, which catches a whole class of bugs at compile time.if/else covers two outcomes. else if chains handle multiple cases, with conditions checked top-to-bottom and only the first matching branch running.if and else bodies, even for single statements. Brace-less bodies cause the dangling-else bug and break the moment someone adds a second line.&& and || makes null-guarded expressions like cart != null && cart.Total > 0 safe. Put the guard on the left side; the right side won't run when the guard already settles the answer.?: picks between two values in a single expression. Use it for short binary choices and inside string interpolation, but avoid nesting more than one.?? and ??= collapse common null-fallback if blocks into one-liners. Reach for them whenever you'd write "if x is null, use y instead" or "if x is null, set it to y."= vs ==, dangling-else without braces, and case-sensitive string equality. Use string.Equals(..., StringComparison.OrdinalIgnoreCase) when case shouldn't matter, and s is null instead of s == null for the safest null check.The next chapter introduces the switch statement, which gives you a cleaner way to dispatch on many discrete values than a long else if ladder.