Verilog Case Statement: casez, casex and Latch Traps

The Verilog case statement is how you describe multi-way selection hardware: multiplexers, decoders, ALU opcode selection, and FSM next-state logic. It looks like a C switch, but it behaves very differently — there is no fall-through, matching is done on all four logic values (0, 1, x, z), and a badly written case statement silently turns your combinational logic into a latch. Interviewers love it for exactly these reasons. This chapter covers plain case, the wildcard forms casez and casex, the default branch, latch inference, full vs parallel case, and when to prefer case over if-else.

How a case statement works

A case statement lives inside a procedural block (always or initial). The general shape:

case (expression)
    item_1:  statement_1;
    item_2:  statement_2;
    item_3,
    item_4:  statement_34;   // one branch for several items
    default: statement_d;    // runs when nothing above matched
endcase

Three rules decide which branch runs:

  • 4-state exact comparison. The case expression is compared with each item bit by bit, including x and z bits. The comparison behaves like the case-equality operator ===, not ==. So 2'b1x matches an item written as 2'b1x — and nothing else.
  • First match wins. Items are evaluated top to bottom and evaluation stops at the first match. There is no fall-through and no break keyword.
  • At most one branch executes. If nothing matches and there is no default, no branch executes at all — the targets keep their old values. That single fact is the root of the latch-inference problem discussed below.

Here is a complete, simulated example — a tiny 2-bit-opcode ALU, the most common place you will write a case statement in real RTL:

// alu_case.v : 2-bit opcode ALU built with a case statement
module mini_alu (
    input  wire [1:0] op,
    input  wire [7:0] a,
    input  wire [7:0] b,
    output reg  [7:0] y
);
    always @(*) begin
        case (op)
            2'b00:   y = a + b;   // ADD
            2'b01:   y = a - b;   // SUB
            2'b10:   y = a & b;   // AND
            default: y = a | b;   // OR (covers 2'b11 and any x/z op)
        endcase
    end
endmodule

module tb_mini_alu;
    reg  [1:0] op;
    reg  [7:0] a, b;
    wire [7:0] y;
    integer i;

    mini_alu dut (.op(op), .a(a), .b(b), .y(y));

    initial begin
        a = 8'd12;
        b = 8'd10;
        for (i = 0; i < 4; i = i + 1) begin
            op = i[1:0];
            #5;
            $display("op=%b  a=%0d b=%0d  ->  y=%0d", op, a, b, y);
        end
        $finish;
    end
endmodule
op=00  a=12 b=10  ->  y=22
op=01  a=12 b=10  ->  y=2
op=10  a=12 b=10  ->  y=8
op=11  a=12 b=10  ->  y=14
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpuv4c__qj/alu_case.v:34: $finish called at 20 (1s)

Note the default handles the last opcode. Writing 2'b11 explicitly and then adding default: y = 8'h00; would also be fine — the point is that every possible value of op assigns y something.

case vs if-else: which one and when

Both describe selection, but they say different things to the synthesis tool:

Aspectcaseif / else if
Natural hardwareParallel multiplexer (when items don’t overlap)Priority chain of 2:1 muxes
Best forDecoding one variable against many values: opcodes, FSM states, address decodeConditions on different signals, or when priority is genuinely intended
TimingRoughly equal delay to every input of the selectorLater conditions sit deeper in the logic chain
ReadabilityOne expression, many values — very scannableGets noisy past 3–4 branches

Practical rule: if every branch tests the same expression against constants, use case. If the branches test unrelated conditions (if (flush) ... else if (stall) ...), or you deliberately want priority, use if-else. Modern synthesis tools will often produce identical gates for simple examples either way, but the intent you communicate to reviewers — and to the tool in harder cases — differs.

casez: wildcard matching done right

casez treats z bits (written z or ?) in the case items as don’t-care positions. This is the clean way to write priority encoders and decoders where only a few bits of the expression matter in each branch. Use ? rather than z in your patterns — it signals “don’t care” to the reader instead of “high impedance”.

// grant_casez.v : fixed-priority grant logic using casez wildcards
module prio_grant (
    input  wire [3:0] req,    // req[3] has highest priority
    output reg  [3:0] grant
);
    always @(*) begin
        casez (req)
            4'b1???: grant = 4'b1000;
            4'b01??: grant = 4'b0100;
            4'b001?: grant = 4'b0010;
            4'b0001: grant = 4'b0001;
            default: grant = 4'b0000;
        endcase
    end
endmodule

module tb_prio_grant;
    reg  [3:0] req;
    wire [3:0] grant;
    integer i;

    prio_grant dut (.req(req), .grant(grant));

    initial begin
        for (i = 0; i < 16; i = i + 1) begin
            req = i[3:0];
            #5;
            $display("req=%b -> grant=%b", req, grant);
        end
        $finish;
    end
endmodule
req=0000 -> grant=0000
req=0001 -> grant=0001
req=0010 -> grant=0010
req=0011 -> grant=0010
req=0100 -> grant=0100
req=0101 -> grant=0100
req=0110 -> grant=0100
req=0111 -> grant=0100
req=1000 -> grant=1000
req=1001 -> grant=1000
req=1010 -> grant=1000
req=1011 -> grant=1000
req=1100 -> grant=1000
req=1101 -> grant=1000
req=1110 -> grant=1000
req=1111 -> grant=1000
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpi_9do8rz/grant_casez.v:30: $finish called at 80 (1s)

Look at the log: req=0111 grants bit 2, req=1011 grants bit 3 — the wildcards plus top-to-bottom matching give you priority behaviour in five readable lines. Writing the same thing with plain case would need all 16 patterns.

One caution: because the wildcard applies to both sides of the comparison, a z on the req bus itself would also match a ? position. In synthesis that is irrelevant (real gates carry 0/1), but in simulation it can mask a floating input, so keep testbench checks for x/z on interfaces.

casex: why most coding guidelines ban it

casex extends the wildcard to x bits as well — in both the case items and the case expression. That second part is the trap: in simulation an x usually means “uninitialised or contended signal — a bug”. With casex, an unknown selector happily matches your first branch and the bug disappears from view. Compare the two side by side:

// casex_trap.v : how casez and casex react to an unknown (x) input
module dec_both (
    input  wire [1:0] sel,
    output reg  [3:0] yz,   // decoded with casez
    output reg  [3:0] yx    // decoded with casex
);
    always @(*) begin
        casez (sel)
            2'b00:   yz = 4'b0001;
            2'b01:   yz = 4'b0010;
            2'b1?:   yz = 4'b0100;
            default: yz = 4'b0000;
        endcase
        casex (sel)
            2'b00:   yx = 4'b0001;
            2'b01:   yx = 4'b0010;
            2'b1?:   yx = 4'b0100;
            default: yx = 4'b0000;
        endcase
    end
endmodule

module tb_dec_both;
    reg  [1:0] sel;
    wire [3:0] yz, yx;

    dec_both dut (.sel(sel), .yz(yz), .yx(yx));

    initial begin
        sel = 2'b00; #5 $display("sel=%b  casez->%b  casex->%b", sel, yz, yx);
        sel = 2'b10; #5 $display("sel=%b  casez->%b  casex->%b", sel, yz, yx);
        sel = 2'b0x; #5 $display("sel=%b  casez->%b  casex->%b   <-- casex matched 00 anyway", sel, yz, yx);
        sel = 2'bxx; #5 $display("sel=%b  casez->%b  casex->%b   <-- casex hides a fully unknown bus", sel, yz, yx);
        $finish;
    end
endmodule
sel=00  casez->0001  casex->0001
sel=10  casez->0100  casex->0100
sel=0x  casez->0000  casex->0001   <-- casex matched 00 anyway
sel=xx  casez->0000  casex->0001   <-- casex hides a fully unknown bus
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpkwi9bgx1/casex_trap.v:34: $finish called at 20 (1s)

With sel = 2'bxx the casez decoder falls to default and outputs a visibly “wrong” value you can catch in verification. The casex decoder outputs 0001 — a perfectly legal-looking result driven by garbage. Gate-level simulation and silicon will not reproduce it. The standard guidance, echoed in most company RTL coding standards: use casez for wildcard matching, avoid casex in synthesisable code.

Latch inference: the classic trap

For a combinational always @(*) block, synthesis demands that every output is assigned on every path through the block. A case statement with missing arms and no default breaks that rule: for the unlisted values the output must “remember” its previous value, and the only combinational element that remembers is a level-sensitive latch. The tool infers one, prints a warning most people scroll past, and your timing closure, testability and glitch behaviour all suffer.

You can watch the stale-value behaviour directly in simulation:

// latch_case.v : same selector written twice - one infers a latch, one does not
module sel_bad (
    input  wire [1:0] sel,
    input  wire [3:0] d0,
    input  wire [3:0] d1,
    output reg  [3:0] y
);
    always @(*) begin
        case (sel)
            2'b00: y = d0;
            2'b01: y = d1;
        endcase   // no default: y keeps its old value for sel = 10 or 11
    end
endmodule

module sel_good (
    input  wire [1:0] sel,
    input  wire [3:0] d0,
    input  wire [3:0] d1,
    output reg  [3:0] y
);
    always @(*) begin
        case (sel)
            2'b00:   y = d0;
            2'b01:   y = d1;
            default: y = 4'b0000;   // every path assigns y: pure combinational
        endcase
    end
endmodule

module tb_latch_demo;
    reg  [1:0] sel;
    reg  [3:0] d0, d1;
    wire [3:0] y_bad, y_good;

    sel_bad  u_bad  (.sel(sel), .d0(d0), .d1(d1), .y(y_bad));
    sel_good u_good (.sel(sel), .d0(d0), .d1(d1), .y(y_good));

    initial begin
        d0 = 4'b1010;
        d1 = 4'b0101;
        sel = 2'b00; #5 $display("sel=%b  y_bad=%b  y_good=%b", sel, y_bad, y_good);
        sel = 2'b01; #5 $display("sel=%b  y_bad=%b  y_good=%b", sel, y_bad, y_good);
        sel = 2'b10; #5 $display("sel=%b  y_bad=%b  y_good=%b   <-- y_bad holds stale d1", sel, y_bad, y_good);
        sel = 2'b11; #5 $display("sel=%b  y_bad=%b  y_good=%b   <-- still stuck", sel, y_bad, y_good);
        $finish;
    end
endmodule
sel=00  y_bad=1010  y_good=1010
sel=01  y_bad=0101  y_good=0101
sel=10  y_bad=0101  y_good=0000   <-- y_bad holds stale d1
sel=11  y_bad=0101  y_good=0000   <-- still stuck
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp4o_griig/latch_case.v:46: $finish called at 20 (1s)

Two reliable ways to stay latch-free in combinational case logic:

  1. Always write a default branch that assigns every output the block drives, or
  2. Assign safe default values to all outputs at the top of the block, before the case statement. Later branches simply override them. This scales better when one case statement drives many signals.

Latch inference applies only to combinational intent. Inside a clocked block (always @(posedge clk)), a case statement with missing arms is fine — “keep the old value” just means the flip-flop holds, which is normal sequential behaviour.

Full case vs parallel case

Two properties of a case statement matter to synthesis, and interviewers frequently ask you to define them:

  • Full case: every value the expression can actually take is covered by some item (or by default). A case that is not full and assigns combinational outputs → latch inference, as above.
  • Parallel case: no input value can match more than one item. Non-overlapping items synthesise to a flat parallel mux; overlapping items (common with casez wildcards, like our priority grant) force priority logic, because the language says the first match wins.

Older flows encouraged pragmas such as // industry-standard EDA tools full_case parallel_case to force these properties. Treat them as red flags in modern code: they tell the synthesis tool to assume something the simulator does not enforce. If the assumption is false — say two casez arms really can match together — synthesis builds hardware that disagrees with your verified RTL simulation. That class of bug is found in gate-level sim at best, in silicon at worst. Make the case genuinely full with a default, and make priority explicit with item ordering, instead of asserting it with pragmas.

Rules you can carry into an interview

  • Case comparison is 4-state and exact (=== semantics); items evaluate top to bottom; first match wins; no fall-through.
  • Combinational case logic: add default or pre-assign all outputs, otherwise a latch is inferred.
  • Use case when one expression is compared against constants; use if-else for unrelated conditions or deliberate priority.
  • Use casez with ? wildcards for decoders and priority encoders; avoid casex because it lets unknown (x) inputs silently match a branch.
  • Avoid full_case/parallel_case pragmas; write code that is genuinely full and has explicit priority.
  • In clocked blocks, missing arms are legal and simply mean “hold the register”.

Quick check

A combinational case statement covers 6 of the 8 possible selector values and has no default. What hardware does synthesis produce for the outputs?

A level-sensitive latch on each assigned output. For the 2 uncovered values no branch assigns the output, so it must hold its previous value, and the only combinational “memory” is a latch. Fix it with a default branch or by assigning every output a safe value at the top of the block.

What is the difference between casez and casex?

casez treats only z (and the ? shorthand) as a don’t-care in the comparison; casex treats both z and x as don’t-cares. Because casex also wildcards x bits in the case expression itself, an uninitialised or contended selector can silently match a branch, hiding real bugs — which is why coding guidelines prefer casez.

When would you choose if-else over a case statement?

When the branches test different, unrelated conditions rather than one expression against constants, or when you explicitly want priority between conditions. A case statement is the better fit when a single expression (opcode, state, address) is decoded against multiple constant values.

Why are the full_case and parallel_case synthesis pragmas considered risky?

They make the synthesis tool assume coverage or non-overlap that the simulator never checks. If the assumption is wrong, the gates no longer match the simulated RTL behaviour, producing a simulation-vs-synthesis mismatch that typically surfaces only in gate-level simulation or in silicon. Writing a genuine default and ordering arms for explicit priority achieves the same result safely.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD