Loading
All articles
Programming 25 min read

The If Statement: A Visual, Hands-On Guide to Decisions in Code

Learn the if statement visually: if, else, else if, && and ||, short-circuiting, the classic bugs (including Apple's goto fail), guard clauses, switch and ?:, real branch-prediction benchmarks, and exercises with answers.

nbkhope
On this page

Every useful program makes decisions. A game checks whether you hit the enemy. A shopping site checks whether the item is in stock. Your phone checks whether the password is right. Behind all of these is the humblest, most important tool in programming: the if statement.

An if lets your program reach a fork in the road and choose a path. In this guide you'll see how it works step by step, chain decisions together, combine conditions, avoid the bugs that have bitten even Apple, and finish with exercises to practice on.

The examples are in C++, but the same if (...) { ... } else { ... } shape works almost unchanged in C, Java, C#, JavaScript, Go, PHP and many other languages. Near the end you'll see it in Python and JavaScript too.

A fork in the road

An if asks a yes-or-no question, called the condition. If the answer is true, the code inside the braces runs. If it's false, that code is skipped. Add an else and the program takes a different path instead of skipping:

An if is a fork in the road, as two flowcharts. On the left, if: do it, or skip it. The condition temperature < 15 is checked: if true, bring a jacket, then have a nice day; if false, skip straight to have a nice day. On the right, if/else: pick one of two paths. The condition number % 2 == 0 is checked: if true, say even; if false, say odd; both paths then carry on. Every condition boils down to true or false. True takes one branch; false takes the other, or skips the block

Just if: do it or skip it

1#include <iostream>
2
3int main() {
4    int temperature;
5    std::cout << "Temperature outside (C): ";
6    std::cin >> temperature;
7
8    if (temperature < 15) {
9        std::cout << "Brrr! Bring a jacket.\n";
10    }
11
12    std::cout << "Have a nice day!\n";
13}

Two sample runs. On a cold day:

Temperature outside (C): 10
Brrr! Bring a jacket.
Have a nice day!

On a warm day, the jacket line is skipped, but "Have a nice day!" always runs, because it's outside the braces:

Temperature outside (C): 22
Have a nice day!

if / else: one of two paths

1#include <iostream>
2
3int main() {
4    int number;
5    std::cout << "Pick a whole number: ";
6    std::cin >> number;
7
8    if (number % 2 == 0) {
9        std::cout << number << " is even.\n";
10    } else {
11        std::cout << number << " is odd.\n";
12    }
13}
Pick a whole number: 7
7 is odd.
Pick a whole number: 42
42 is even.

% gives the remainder of a division, so number % 2 == 0 means "divides by 2 with nothing left over". Exactly one of the two blocks runs, every time. Never both, never neither.

Conditions: questions with a true-or-false answer

A condition is any expression that ends up true or false. Most are built with comparison operators:

OperatorMeansExampleWith age = 15
==equal toage == 15true
!=not equal toage != 15false
<less thanage < 13false
<=less than or equalage <= 15true
>greater thanage > 18false
>=greater than or equalage >= 13true

else if: choosing between many paths

When there are more than two possibilities, chain conditions with else if. The program checks them from top to bottom and runs the block of the first one that's true. Everything after that is skipped:

else if: the first true condition wins, the rest are skipped. With score = 83, the chain checks score >= 90 first: false. Then score >= 80: true, so it returns B. The checks for 70 and 60, and the final else that gives F, are skipped. Only one branch ever runs. For 83, two conditions are checked and the other three branches are never even looked at

Here's the original example from this article, now as a complete program:

1#include <iostream>
2
3int main() {
4    int number;
5    std::cout << "Enter a number between 1 and 100: ";
6    std::cin >> number;
7
8    if (number < 1) {
9        std::cout << "Your number is too small.\n";
10    } else if (number > 100) {
11        std::cout << "Your number is too big.\n";
12    } else {
13        // Both checks failed, so the number must be from 1 to 100
14        std::cout << "You are in the right range!\n";
15    }
16}

Three sample runs, one for each path:

Enter a number between 1 and 100: -5
Your number is too small.
Enter a number between 1 and 100: 150
Your number is too big.
Enter a number between 1 and 100: 42
You are in the right range!

Notice the else at the end has no condition. It doesn't need one: if the number isn't below 1 and isn't above 100, there's only one possibility left. The else is the last resort, the path taken when every other door is closed.

A grade ladder

Converting a score into a letter grade is the classic else-if ladder:

1#include <iostream>
2
3char letterGrade(int score) {
4    if (score >= 90) {
5        return 'A';
6    } else if (score >= 80) {
7        return 'B';
8    } else if (score >= 70) {
9        return 'C';
10    } else if (score >= 60) {
11        return 'D';
12    } else {
13        return 'F';
14    }
15}
16
17int main() {
18    int scores[] = {95, 83, 71, 64, 42, 90, 89};
19    for (int score : scores) {
20        std::cout << score << " -> " << letterGrade(score) << "\n";
21    }
22}
195 -> A
283 -> B
371 -> C
464 -> D
542 -> F
690 -> A
789 -> B

Why doesn't 95 also get a B, since 95 is also >= 80? Because the chain stops at the first true condition. That's also why the order of the checks matters so much.

Order matters

Watch what happens when the same five tests are written in the opposite order:

1#include <iostream>
2
3char letterGrade(int score) {
4    if (score >= 60) {          // Oops: the easiest test comes first
5        return 'D';
6    } else if (score >= 70) {
7        return 'C';
8    } else if (score >= 80) {
9        return 'B';
10    } else if (score >= 90) {
11        return 'A';
12    } else {
13        return 'F';
14    }
15}
16
17int main() {
18    int scores[] = {95, 83, 71, 64, 42};
19    for (int score : scores) {
20        std::cout << score << " -> " << letterGrade(score) << "\n";
21    }
22}
195 -> D
283 -> D
371 -> D
464 -> D
542 -> F
Same tests, wrong order: everybody gets a D. With the strictest test first, 95 gets A, 83 gets B, 71 gets C, 64 gets D and 42 gets F. With the easiest test, >= 60, first, 95, 83, 71 and 64 all get D, and only 42 gets F. 95 passes >= 60 right away, so the chain stops there. With overlapping ranges, put the strictest test first

It compiles without a single warning. The computer did exactly what it was told. This is a logic bug, the hardest kind to spot, which is why testing with several sample inputs matters.

Combining conditions: &&, || and !

Real decisions often depend on more than one thing. C++ has three logical operators to combine conditions:

OperatorNameTrue when…Example
&&andboth sides are trueage >= 13 && hasPermission
||orat least one side is trueisWeekend || isHoliday
!notthe condition is false!isLoggedIn

The full truth table, with every combination:

aba && ba || b!a
truetruetruetruefalse
truefalsefalsetruefalse
falsetruefalsetruetrue
falsefalsefalsefalsetrue

Example: is it a leap year?

The leap-year rule is a great workout for logical operators. A year is a leap year if it divides by 4, except years that divide by 100, unless they also divide by 400:

Is it a leap year? A decision tree. Divisible by 400? If yes, leap year, like 2000. If no: divisible by 100? If yes, not a leap year, like 1900 and 2100. If no: divisible by 4? If yes, leap year, like 2024. If no, not a leap year, like 2023. In one line of code: (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
1#include <iostream>
2
3int main() {
4    int years[] = {2023, 2024, 1900, 2000, 2100};
5    for (int year : years) {
6        bool isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
7        std::cout << year << (isLeapYear ? " is" : " is not") << " a leap year\n";
8    }
9}
12023 is not a leap year
22024 is a leap year
31900 is not a leap year
42000 is a leap year
52100 is not a leap year

Short-circuiting: the right side might never run

&& and || are lazy, in a good way. They evaluate the left side first, and if that already decides the answer, they skip the right side completely:

Short-circuit: && and || stop as soon as the answer is known. In gamesPlayed > 0 && totalPoints / gamesPlayed > 10, the left side is false, so the right side is never evaluated: false && anything is false, and there's no division by zero. In isAdmin || hasPermission(user), the left side is true, so the right side is never evaluated: true || anything is true. Put the cheap or protective check on the left. It guards the expensive or risky one on the right

This is extremely handy for guarding against errors:

1#include <iostream>
2
3int main() {
4    int totalPoints = 120;
5    int gamesPlayed = 0;
6
7    // && stops at the first false: the division never happens when gamesPlayed is 0
8    if (gamesPlayed > 0 && totalPoints / gamesPlayed > 10) {
9        std::cout << "High scorer!\n";
10    } else {
11        std::cout << "Not enough games yet.\n";
12    }
13}
Not enough games yet.

Swap the two sides and the program would divide by zero, which in C++ is undefined behavior and on most computers crashes the program. Same conditions, different order, completely different result.

The classic if bugs

These three mistakes have cost programmers countless hours. Learn to spot them now and you'll save yourself the trouble.

Bug 1: = instead of ==

1#include <iostream>
2
3int main() {
4    int lives = 3;
5    if (lives = 0) {
6        std::cout << "Game over\n";
7    }
8    std::cout << "Lives left: " << lives << "\n";
9}
Lives left: 0

lives = 0 doesn't compare anything. It stores 0 in lives, and the whole expression has the value 0, which counts as false. So the player silently loses all their lives and the game-over message never appears. Fortunately, with warnings on (g++ -Wall), GCC 14 catches it:

1assign.cpp: In function 'int main()':
2assign.cpp:5:15: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
3    5 |     if (lives = 0) {
4      |         ~~~~~~^~~

Bug 2: missing braces

Without braces, an if controls only the next single statement, no matter how the code is indented:

1#include <iostream>
2
3int main() {
4    int balance = 50;
5    int price = 80;
6    if (balance < price)
7        std::cout << "Not enough money.\n";
8        std::cout << "Purchase cancelled.\n";
9    std::cout << "Thanks for shopping!\n";
10}

That looks fine. Now imagine the balance were 100: "Purchase cancelled." would still print, because it isn't part of the if at all. The indentation lies. GCC warns about this too:

1indent.cpp: In function 'int main()':
2indent.cpp:6:5: warning: this 'if' clause does not guard... [-Wmisleading-indentation]
3    6 |     if (balance < price)
4      |     ^~
5indent.cpp:8:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the 'if'
6    8 |         std::cout << "Purchase cancelled.\n";
7      |         ^~~

The real-world version: Apple's "goto fail"

In February 2014, Apple shipped an urgent security update for iPhones, iPads and Macs. The cause was one duplicated line in the code that checks secure (HTTPS) connections:

One extra line: Apple's 2014 goto fail bug. A series of if statements each check a step of a secure connection and, on error, goto fail. Each goto fail line is guarded by the if above it, except one duplicated goto fail line that is NOT guarded and always runs. The final check after it is never reached. Without braces, an if guards only the next statement. The duplicated line always jumped past the final check, so iPhones and Macs accepted forged secure connections until it was fixed in February 2014

The second goto fail; isn't controlled by any if, so it always runs, jumping past the final signature check. The result: devices could be tricked into trusting a fake website. It's exactly Bug 2, in software used by hundreds of millions of people.

Bug 3: wrong order in an else-if chain

You've already met this one: the "everybody gets a D" grade ladder above. When ranges overlap, the strictest test goes first.

Cleaner decisions

Guard clauses instead of deep nesting

Ifs inside ifs inside ifs quickly drift to the right of the screen, a shape programmers call arrow code. A guard clause handles each problem up front and leaves early:

Deep nesting vs. guard clauses. On the left, nested code: if logged in, if items in cart, if has address, place order, each with its own else, forming an arrow shape that drifts right. On the right, guard clauses: if not logged in, return ask to log in; if the cart is empty, return show empty cart; if no address, return ask for address; then place order, flat at the bottom. Both do the same thing. Guard clauses deal with each problem and leave early, so the main action sits at the bottom, unindented
1#include <iostream>
2#include <string>
3
4std::string checkout(bool loggedIn, int itemsInCart, bool hasAddress) {
5    if (!loggedIn) return "Please log in first.";
6    if (itemsInCart == 0) return "Your cart is empty.";
7    if (!hasAddress) return "Add a shipping address.";
8    return "Order placed!";
9}
10
11int main() {
12    std::cout << checkout(false, 2, true) << "\n";
13    std::cout << checkout(true, 0, true) << "\n";
14    std::cout << checkout(true, 2, false) << "\n";
15    std::cout << checkout(true, 2, true) << "\n";
16}
1Please log in first.
2Your cart is empty.
3Add a shipping address.
4Order placed!

Each line reads like a rule, and the happy path, "Order placed!", sits at the bottom where you'd expect it.

The ternary operator ? :

When all you need is to pick one of two values, the conditional (ternary) operator is a compact if/else:

1#include <iostream>
2#include <string>
3
4int main() {
5    for (int count = 0; count <= 2; count++) {
6        std::string word = (count == 1) ? "item" : "items";
7        std::cout << "You have " << count << " " << word << " in your cart\n";
8    }
9}
You have 0 items in your cart
You have 1 item in your cart
You have 2 items in your cart

Read condition ? a : b as "if condition, then a, otherwise b". Keep it for short choices; nested ternaries quickly become unreadable.

switch: one variable, many fixed values

When you compare one variable against a list of specific values, switch can be tidier than a long else-if chain:

1#include <iostream>
2
3int main() {
4    for (int day = 1; day <= 7; day++) {
5        std::cout << "Day " << day << ": ";
6        switch (day) {
7            case 6:
8            case 7:
9                std::cout << "weekend!\n";
10                break;
11            case 5:
12                std::cout << "almost there\n";
13                break;
14            default:
15                std::cout << "school day\n";
16        }
17    }
18}
1Day 1: school day
2Day 2: school day
3Day 3: school day
4Day 4: school day
5Day 5: almost there
6Day 6: weekend!
7Day 7: weekend!

default plays the role of else. Don't forget break: without it, execution "falls through" into the next case (which is exactly what case 6: does on purpose here, sharing the weekend message with case 7:).

Which one should you use?

Which decision tool fits? if: do something, or skip it, as in if (raining) takeUmbrella();. if/else: exactly one of two paths. else if: one of many, tested in order, like grade ladders and ranges. switch: one variable compared against fixed values, like switch (day). The ternary ? :: pick one of two values, like count == 1 ? item : items. && and ||: combine conditions, like age >= 13 && hasPermission

Under the hood: why the processor guesses

Here's something most beginners never hear about. Modern processors work on many instructions at once, like an assembly line. When they reach an if, they don't wait to find out which way it goes; they guess and keep working. A right guess is free. A wrong guess means throwing away the half-finished work and starting over. This is called branch prediction.

We measured it for this article. The program adds up every value of 128 or more among 32 million random numbers from 0 to 255. Then it sorts the same numbers and runs the exact same if again:

The same if, 11 times slower on random data. Adding up the values >= 128 among 32 million random numbers from 0 to 255, shuffled vs. sorted, measured for this article with GCC 14.1, median of 7 runs. With a real branch, compiled with -O2 and if-conversion off: random order 116.9 milliseconds, sorted 10.4 milliseconds. When the compiler removed the branch, plain -O2: random order 17.6 milliseconds, sorted 19.0 milliseconds. The processor guesses which way each if will go. On sorted data it guesses right almost every time; on random data, half its guesses are wrong, and each miss throws away work. Here GCC normally swaps the branch for a branch-free instruction, which is why plain -O2 doesn't care about the order

With a real branch, the random data took 11 times longer: same numbers, same if, same answer. On sorted data, the if goes "no, no, no… yes, yes, yes", so the processor's guesses are almost always right. On random data it's a coin flip.

There's a twist: with normal optimization, GCC noticed the pattern and replaced the if with a special branch-free instruction (a conditional move), so the order stopped mattering. Compilers are clever, but they can't always do that, which is why fast code tries to make its decisions predictable.

A habit with the same spirit: common cases first

In an else-if chain where the cases don't overlap, putting the most common case first means fewer checks overall. We counted the checks for a million simulated chat messages:

Order your else-ifs by how often they happen. 1,000,000 chat messages: 80% text, 12% photo, 5% video, 3% voice. Average conditions checked per message, counted by a C++ program. Most common first, checking text, photo, video, else voice: 1.28 checks. Rarest first, checking voice, video, photo, else text: 2.89 checks. Same answers, 2.3 times more checks. It rarely matters for speed, but it's a good habit, and it's exactly the thinking behind the branch-prediction chart above

The same idea in other languages

The if statement is nearly universal. Here's the range check in two other popular languages.

Python uses elif, a colon and indentation instead of braces:

1number = 42
2if number < 1:
3    print("Your number is too small.")
4elif number > 100:
5    print("Your number is too big.")
6else:
7    print("You are in the right range!")

JavaScript looks almost identical to C++:

1const number = 42;
2if (number < 1) {
3  console.log("Your number is too small.");
4} else if (number > 100) {
5  console.log("Your number is too big.");
6} else {
7  console.log("You are in the right range!");
8}

Both print You are in the right range!.

C, C++, Java, C#JavaScriptPython
Else-ifelse ifelse ifelif
Block{ ... }{ ... }indentation
And / or / not&& || !&& || !and or not
Equality===== (recommended)==

Practice time

Try these before peeking at the answers. Copy each program into a file, compile with g++ -std=c++20 -Wall program.cpp, and run it.

Exercise 1: Predict the output (warm-up). What does this print?

1int coins = 7;
2if (coins > 10) {
3    std::cout << "rich ";
4} else if (coins > 5) {
5    std::cout << "comfortable ";
6} else if (coins > 0) {
7    std::cout << "scraping by ";
8}
9if (coins % 2 == 1) {
10    std::cout << "odd";
11}
12std::cout << "\n";

Exercise 2: FizzBuzz. The most famous programming interview warm-up. For the numbers 1 to 15, print Fizz for multiples of 3, Buzz for multiples of 5, FizzBuzz for multiples of both, and the number itself otherwise. (Watch the order of your conditions!)

Exercise 3: Movie tickets. Write int ticketPrice(int age, bool isTuesday): under 3 is free; children under 12 and seniors 65 and over pay $8; everyone else pays $10 on Tuesdays and $15 on other days.

Exercise 4: Rock, paper, scissors. Write a function that takes two moves ('R', 'P' or 'S') and returns "Tie", "Player 1 wins" or "Player 2 wins". Try to do it with a single || chain instead of nine separate ifs.

Exercise 5: Triangle detective (challenge). Given three side lengths, print equilateral (all equal), isosceles (two equal), scalene (all different) or not a triangle (when any two sides add up to no more than the third).

Quick quiz

  1. What's the difference between x = 5 and x == 5?
  2. In an if / else if / else chain, can two blocks run?
  3. In if (b != 0 && a / b > 2), what happens when b is 0?
  4. What does !(age >= 18) mean in plain English?
  5. Why do style guides insist on braces even around one-line ifs?

Answers

Exercise 1: comfortable odd. The else-if chain stops at coins > 5. The second if is separate, not part of the chain, so it's checked too: 7 is odd.

Exercise 2: The "both" case must come first; otherwise 15 would match % 3 and print only Fizz.

1#include <iostream>
2
3int main() {
4    for (int number = 1; number <= 15; number++) {
5        if (number % 15 == 0) {
6            std::cout << "FizzBuzz\n";
7        } else if (number % 3 == 0) {
8            std::cout << "Fizz\n";
9        } else if (number % 5 == 0) {
10            std::cout << "Buzz\n";
11        } else {
12            std::cout << number << "\n";
13        }
14    }
15}
11
22
3Fizz
44
5Buzz
6Fizz
77
88
9Fizz
10Buzz
1111
12Fizz
1313
1414
15FizzBuzz

Exercise 3: Guard clauses keep it short. Babies come first, since "under 12" would also match them.

1#include <iostream>
2
3int ticketPrice(int age, bool isTuesday) {
4    if (age < 3) return 0;
5    if (age < 12 || age >= 65) return 8;
6    if (isTuesday) return 10;
7    return 15;
8}
9
10int main() {
11    std::cout << "Baby, age 2:          $" << ticketPrice(2, false) << "\n";
12    std::cout << "Kid, age 9:           $" << ticketPrice(9, false) << "\n";
13    std::cout << "Adult, Tuesday:       $" << ticketPrice(30, true) << "\n";
14    std::cout << "Adult, Saturday:      $" << ticketPrice(30, false) << "\n";
15    std::cout << "Grandparent, age 70:  $" << ticketPrice(70, true) << "\n";
16}
1Baby, age 2:          $0
2Kid, age 9:           $8
3Adult, Tuesday:       $10
4Adult, Saturday:      $15
5Grandparent, age 70:  $8

Exercise 4: Handle the tie, then list the three ways player 1 can win. Anything else means player 2 won.

1#include <iostream>
2#include <string>
3
4std::string winner(char first, char second) {
5    if (first == second) return "Tie";
6    if ((first == 'R' && second == 'S') ||
7        (first == 'S' && second == 'P') ||
8        (first == 'P' && second == 'R')) {
9        return "Player 1 wins";
10    }
11    return "Player 2 wins";
12}
13
14int main() {
15    std::cout << "R vs S: " << winner('R', 'S') << "\n";
16    std::cout << "P vs S: " << winner('P', 'S') << "\n";
17    std::cout << "P vs P: " << winner('P', 'P') << "\n";
18}
R vs S: Player 1 wins
P vs S: Player 2 wins
P vs P: Tie

Exercise 5: Check for impossible triangles first, then the strictest shape (all three equal) before the looser one (any two equal).

1#include <iostream>
2#include <string>
3
4std::string triangleType(int first, int second, int third) {
5    if (first + second <= third || first + third <= second || second + third <= first) {
6        return "not a triangle";
7    }
8    if (first == second && second == third) return "equilateral";
9    if (first == second || second == third || first == third) return "isosceles";
10    return "scalene";
11}
12
13int main() {
14    std::cout << "3 3 3: " << triangleType(3, 3, 3) << "\n";
15    std::cout << "5 5 8: " << triangleType(5, 5, 8) << "\n";
16    std::cout << "3 4 5: " << triangleType(3, 4, 5) << "\n";
17    std::cout << "1 2 10: " << triangleType(1, 2, 10) << "\n";
18}
13 3 3: equilateral
25 5 8: isosceles
33 4 5: scalene
41 2 10: not a triangle

Quiz: (1) x = 5 stores 5 in x; x == 5 asks whether x equals 5. (2) No: at most one block runs, and with a final else, exactly one. (3) Nothing bad: b != 0 is false, so && short-circuits and the division never happens. (4) "Under 18". (5) Without braces, an if controls only the next statement, and misleading indentation (as in Apple's "goto fail") can hide that.

Cheat sheet

I want to…Write
Do something only sometimesif (condition) { ... }
Choose between two pathsif (condition) { ... } else { ... }
Choose between many pathsif (a) { ... } else if (b) { ... } else { ... }
Require both conditionsa && b
Accept either conditiona || b
Flip a condition!a
Pick one of two valuescondition ? valueIfTrue : valueIfFalse
Compare one variable to fixed valuesswitch (variable) { case 1: ... break; default: ... }
Bail out earlyif (problem) return;
Name a complicated conditionbool isLeapYear = ...; if (isLeapYear) { ... }

Wrapping up

The if statement is how a program makes decisions, and almost every program is full of them:

  • if runs a block only when its condition is true; else gives the alternative path.
  • else if checks conditions top to bottom, and the first true one wins. Order matters.
  • &&, || and ! combine conditions, and they short-circuit, which you can use to guard risky checks.
  • Watch for the classics: = vs ==, missing braces (remember "goto fail"), and overlapping conditions in the wrong order. Compiler warnings catch the first two.
  • Guard clauses, ? : and switch keep decision code flat and readable.
  • Predictable decisions are fast: in our test, the same if ran 11 times faster on sorted data.

Decisions plus repetition are the heart of programming. Next, see how ifs team up with loops in The For Loop and The While and Do-While Loops, meet the if statement's cousin in The Switch Statement (Case), and put them to work on data with C++ Arrays.

Want a more in-depth look into computer programming? Check out The Self-Taught Programmer.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.