Back to all posts

[Flags] enums in .NET: the rules that keep your bits honest

How [Flags] enums really work in C#, the traps that turn them into silent bugs, and the rules and tests that prevent them.

October 26, 2026

[Flags] enums in .NET: the rules that keep your bits honest

A [Flags] enum looks like the simplest thing in C#. A few names, a few numbers, an attribute on top. It is also one of the easiest places to ship a bug that compiles, passes code review, passes most test suites, and quietly gives the wrong answer in production.

The bug is never in the attribute. It is in the values you give the members, and in the assumptions you make when you read them back. This article covers what [Flags] actually does, the four rules that keep flag enums honest, and a unit test that stops the most common mistake from ever reaching the main branch.

What [Flags] actually does

An ordinary enum is a choice from a list of mutually exclusive values: a season, a status, a log level. A flags enum is a set of options that can be on at the same time: the channels a user wants notifications on, the access rights on a file, the features enabled for an account.

The FlagsAttribute documentation describes it as indicating “that an enumeration can be treated as a bit field; that is, a set of flags.” You combine options with | and test for them with &:

[Flags]
enum Channels
{
    None  = 0,
    Email = 1,
    Sms   = 2,
    Push  = 4,
}

var prefs = Channels.Email | Channels.Push;   // 0b101
bool wantsSms = (prefs & Channels.Sms) == Channels.Sms;  // false

Here is the part that surprises people: the attribute does not make your values combinable. Bitwise operators work on any enum, with or without it. What [Flags] changes is mostly how a value is displayed. Without it, the combined value above prints as 5. With it, ToString() prints Email, Push. The attribute is a statement of intent, and a formatting hint. Making the values actually work as independent bits is your job.

Rule 1: every member owns its own bit

C# numbers enum members for you if you don’t. The C# language reference is explicit: values “start with zero and increase by one following the definition text order.” The attribute does not change that.

So this enum:

[Flags]
enum Channels { None, Email, Sms, Push }

really means None = 0, Email = 1, Sms = 2, Push = 3. And 3 is not a bit of its own. It is 1 | 2.

Watch what that does to a perfectly reasonable check:

var prefs = Channels.Email | Channels.Sms;   // 1 | 2 == 3

prefs.HasFlag(Channels.Push);                // true

The user opted into email and SMS. The code says they also opted into push. Nothing threw, nothing logged, and any test that only checks one channel at a time still passes.

The fix is the guideline the FlagsAttribute page states directly: “Define enumeration constants in powers of two, that is, 1, 2, 4, 8, and so on. This means the individual flags in combined enumeration constants do not overlap.”

Two ways to write that so the intent is visible at a glance:

[Flags]
enum Channels
{
    None  = 0,
    Email = 1 << 0,
    Sms   = 1 << 1,
    Push  = 1 << 2,
}

[Flags]
enum Channels
{
    None  = 0b_0000,
    Email = 0b_0001,
    Sms   = 0b_0010,
    Push  = 0b_0100,
}

Shifts make the bit index obvious. Binary literals, which the C# reference itself uses for its flags example, make the bit pattern obvious. Either way, the next person to add a member can see which bit is free.

When this stops being a cosmetic bug

A notification sent to the wrong channel is annoying. The same mistake in a roles or permissions enum is a security problem. If Admin happens to equal Reader | Writer, then every user who holds both of those roles passes an Admin check. Authorization code tends to branch on exactly this kind of test, and it tends to be covered by tests that check one role at a time. The overlap is invisible until someone looks at the numbers.

Rule 2: name your zero, and never test for it with HasFlag

The FlagsAttribute guidelines recommend: “Use None as the name of the flag enumerated constant whose value is zero.” There is a good reason beyond tidiness. The C# reference notes that the default value of any enum is (E)0, “even if zero doesn’t have the corresponding enum member.” A field you never assigned is zero. Give that state a name, so it means something on purpose.

Then never check for it like this:

prefs.HasFlag(Channels.None);   // always true

The HasFlag documentation defines the method as (thisInstance & flag) == flag, and states: “If the underlying value of flag is zero, the method returns true.” Any value ANDed with zero is zero, so the comparison always holds. The FlagsAttribute guidelines say the same about the bitwise form: you “cannot use the None enumerated constant in a bitwise AND operation to test for a flag because the result is always zero.”

To ask “is nothing set?”, compare for equality:

bool optedOut = prefs == Channels.None;

Rule 3: name combinations on purpose

Combinations are not the enemy. Accidental combinations are. The guidelines suggest the opposite of avoiding them: “Consider creating an enumerated constant for commonly used flag combinations,” with the example of ReadWrite = Read | Write.

[Flags]
enum Channels
{
    None  = 0,
    Email = 1 << 0,
    Sms   = 1 << 1,
    Push  = 1 << 2,
    All   = Email | Sms | Push,
}

The difference between All here and Push = 3 in the broken version is that All is defined as a combination. Anyone reading the enum sees that it is one. Nobody will mistake it for an independent option.

HasFlag also works naturally with a combination as its argument: prefs.HasFlag(Channels.Email | Channels.Sms) is true only when both bits are set.

Rule 4: validate anything that comes from outside

An enum type is not a guarantee about its value. The C# reference shows that any integer can be cast to an enum, and the FlagsAttribute guidelines warn: “you can cast a numeric value to the enumeration type even if that numeric value is not defined in the enumeration.” That matters the moment a flags value arrives from a request body, a query string, a database column or a message queue.

Enum.Parse is more permissive than most people expect. Its documentation says the input can be “a list of named constants delimited by commas,” and “the return value is the value of the specified names combined with a bitwise OR operation.” It also accepts numbers that match no member: in that case “the method returns an enumeration member whose underlying value is value converted to an integral type.”

Enum.Parse<Channels>("Email, Sms");   // Email | Sms
Enum.Parse<Channels>("64");           // (Channels)64: no member, no exception

If your input validation assumes a single name or a known number, both of those walk straight through it.

The obvious guard doesn’t work for flags either. Enum.IsDefined checks whether a value matches a named constant, and its documentation is explicit: for bit-field enums “the method determines only whether a single bit field belongs to the enumeration.”

Enum.IsDefined(Channels.Email | Channels.Sms);   // false, yet it is perfectly valid

What you actually want to know is whether the value uses only bits that the enum defines. Build a mask of every known bit once, and check that nothing falls outside it:

static class ChannelRules
{
    private static readonly Channels Known =
        Enum.GetValues<Channels>().Aggregate(Channels.None, (all, c) => all | c);

    public static bool IsValid(Channels value) => (value & ~Known) == 0;
}

ChannelRules.IsValid(Channels.Email | Channels.Sms);   // true
ChannelRules.IsValid((Channels)64);                     // false

Enum.GetValues<TEnum>() returns the value of every member and is available from .NET 5. On older frameworks, use Enum.GetValues(typeof(Channels)) and cast.

Run this check at the boundary, where the value enters your system, and reject what fails. Everything inside can then trust the type.

A unit test that catches overlapping flags

Code review is a poor tool for spotting that the fourth member of an enum is 3. A test is better, because it runs every time someone adds a member.

The rule to encode: every member is zero, a single bit, or a combination you named on purpose.

using System.Numerics;
using Xunit;

public class ChannelsTests
{
    [Fact]
    public void Every_channel_owns_its_own_bit()
    {
        Channels[] namedCombinations = [Channels.All];

        var offenders = Enum.GetValues<Channels>()
            .Where(c => c != Channels.None)
            .Where(c => !BitOperations.IsPow2((int)c))
            .Where(c => !namedCombinations.Contains(c))
            .ToArray();

        Assert.Empty(offenders);
    }
}

BitOperations.IsPow2 returns true when a value is a power of two, which is exactly “a single bit.” It is available from .NET 6.

The explicit list of combinations is the important part. A test that accepted any value whose bits are covered by other members would pass on the broken enum, because 3 is covered by 1 and 2. Making intended combinations explicit is what lets the test tell All apart from a Push that was numbered by accident.

If you have many flags enums, the same check generalizes into a single parameterized test that takes the enum type and its allowed combinations.

HasFlag or bitwise AND?

Both are correct. HasFlag is documented as returning the result of (thisInstance & flag) == flag, so the two forms ask the same question. Pick the one your team reads more easily and use it consistently.

Two cautions apply to both forms. Testing for None always returns true, as covered in Rule 2. And neither form can protect you from overlapping values. If Push is 3, both will tell you a user with email and SMS wants push. The only fix for that is in the enum declaration itself.

Summary

  • [Flags] states intent and changes formatting. It does not assign values for you, and C# numbers unassigned members 0, 1, 2, 3.
  • Give every member its own bit: powers of two, written as shifts or binary literals.
  • Name the zero value None, compare it with ==, and never pass it to HasFlag.
  • Name intended combinations, like All or ReadWrite, inside the enum.
  • Treat flags from outside your process as untrusted: Enum.Parse accepts comma lists and undefined numbers, and Enum.IsDefined rejects valid combinations. Validate against a mask of known bits instead.
  • Add a unit test that fails when a member is neither a single bit nor a named combination.

None of these rules is hard. The trouble with flags enums is that breaking them costs nothing at compile time, and the bill arrives later, as a value that looks right and means something else.