Verilog if-else Statement: Syntax, Priority and Latches

Every if-else you write in Verilog turns into real hardware. In C, an if decides which instructions a CPU executes next; in Verilog, it decides which wires feed a multiplexer, how deep a priority chain goes, and — if you are careless — whether the synthesizer quietly inserts a latch you never asked for. That last part is why “explain inferred latches” is one of the most common questions in VLSI design interviews. This chapter covers the syntax, if-else-if chains, nesting, the latch problem and its fixes, and when you should reach for case instead.

Basic Syntax and Rules

An if-else is a procedural statement, so it is only legal inside procedural blocks: always, initial, tasks and functions. You cannot write a bare if between module ports and an assign statement (for that, use the conditional operator ? :).

always @(*) begin
  if (condition)
    y = a;              // runs when condition is true (non-zero)
  else
    y = b;              // runs otherwise

  if (a > b) begin      // begin...end groups multiple statements
    max  = a;
    diff = a - b;
  end
  else begin
    max  = b;
    diff = b - a;
  end
end

Three rules matter here:

  • The condition is any expression. Verilog treats a non-zero value as true. if (count) is legal and means if (count != 0).
  • Unknowns count as false. If the condition evaluates to x or z, the else branch runs. This is a frequent source of simulation-vs-silicon mismatch, because real gates never carry an x. Keep === comparisons (which match x literally) for testbenches only.
  • Use begin...end for more than one statement. Without it, only the first statement belongs to the branch. Many teams require begin...end on every branch regardless — it costs nothing and prevents an entire class of copy-paste bugs.

if-else-if Chains: How Priority Logic Is Born

Chaining conditions with else if creates a strict order of evaluation: the second condition is only examined when the first is false, the third only when the first two are false, and so on. In hardware terms, the synthesizer builds a cascade of 2-to-1 multiplexers — a priority chain where the first listed condition always wins.

A classic use is an arbiter or priority encoder. Here, four request lines compete and the lowest-numbered requester is granted:

// priority_grant.v
module priority_grant (
  input  wire [3:0] req,
  output reg  [1:0] grant,
  output reg        valid
);
  always @(*) begin
    valid = 1'b1;
    if (req[0])
      grant = 2'd0;
    else if (req[1])
      grant = 2'd1;
    else if (req[2])
      grant = 2'd2;
    else if (req[3])
      grant = 2'd3;
    else begin
      grant = 2'd0;
      valid = 1'b0;
    end
  end
endmodule

// Testbench
module tb_priority_grant;
  reg  [3:0] req;
  wire [1:0] grant;
  wire       valid;

  priority_grant dut (.req(req), .grant(grant), .valid(valid));

  initial begin
    $display("  req  | grant valid");
    req = 4'b0000; #1 $display(" %b |   %0d     %b", req, grant, valid);
    req = 4'b1000; #1 $display(" %b |   %0d     %b", req, grant, valid);
    req = 4'b0100; #1 $display(" %b |   %0d     %b", req, grant, valid);
    req = 4'b0110; #1 $display(" %b |   %0d     %b  <- req[1] beats req[2]", req, grant, valid);
    req = 4'b1111; #1 $display(" %b |   %0d     %b  <- req[0] beats everyone", req, grant, valid);
    $finish;
  end
endmodule
req  | grant valid
 0000 |   0     0
 1000 |   3     1
 0100 |   2     1
 0110 |   1     1  <- req[1] beats req[2]
 1111 |   0     1  <- req[0] beats everyone
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp0ef2_kys/priority_grant.v:39: $finish called at 5 (1s)

Look at the req = 4'b1111 line in the log: all four lines request, but grant is 0 because req[0] is checked first. Two consequences follow for real designs:

  • Order is a design decision. Reorder the branches and you reorder the priority. Put the most urgent (or most common) condition first deliberately, not by accident.
  • Long chains make slow logic. Each else if adds a mux level, so a 16-way chain creates a 16-deep path that can wreck timing. If the conditions are mutually exclusive anyway, a case statement usually produces flatter, faster logic.

Also notice the trick on the first line of the block: valid is assigned a default of 1'b1 before the chain, so only the final else needs to override it. Default-then-override keeps long chains readable and, as we will see next, is also your best defense against latches.

Nested if-else and the Dangling else

An if may appear inside another if. Nesting expresses an AND of conditions with different priorities and is common in control logic — check an enable first, then decide the detail. The trap is the dangling else: when braces are omitted, an else pairs with the nearest unmatched if, not the one your indentation suggests.

// Misleading indentation: the else belongs to if (ready),
// NOT to if (mode), whatever the layout implies
if (mode)
  if (ready)
    y = a;
else              // pairs with if (ready)!
  y = b;

// Say what you mean with begin...end
if (mode) begin
  if (ready)
    y = a;
end
else begin        // now it clearly pairs with if (mode)
  y = b;
end

Keep nesting shallow — two levels is usually the comfortable limit. Deeper decisions are better rewritten as an if-else-if chain with compound conditions (if (mode && ready)) or as a case on a concatenation of the control bits.

Inferred Latches: The Classic if-else Trap

Here is the rule that interviewers want to hear stated precisely: in a combinational always block, every output must be assigned on every possible path through the block. If some path leaves an output unassigned, the output must remember its previous value — and the only combinational element that remembers is a level-sensitive latch. The synthesizer infers one, reports a warning most people scroll past, and your “combinational” block now has state, timing-analysis headaches, and testability problems.

The following simulation puts a latch-inferring block and a clean block side by side, driving them with identical inputs:

// latch_demo.v
module latchy (
  input  wire       sel,
  input  wire [7:0] a,
  output reg  [7:0] y
);
  always @(*) begin
    if (sel)
      y = a;        // no else: y must hold its old value -> latch
  end
endmodule

module clean (
  input  wire       sel,
  input  wire [7:0] a,
  output reg  [7:0] y
);
  always @(*) begin
    if (sel)
      y = a;
    else
      y = 8'h00;    // every path assigns y -> pure combinational
  end
endmodule

// Testbench
module tb_latch_demo;
  reg        sel;
  reg  [7:0] a;
  wire [7:0] y_latchy, y_clean;

  latchy u1 (.sel(sel), .a(a), .y(y_latchy));
  clean  u2 (.sel(sel), .a(a), .y(y_clean));

  initial begin
    $display(" sel  a  | y_latchy y_clean");
    sel = 1; a = 8'hA5; #1;
    $display("  %b   %h |    %h       %h", sel, a, y_latchy, y_clean);
    sel = 0; a = 8'h3C; #1;
    $display("  %b   %h |    %h       %h   <- latchy remembers stale a5", sel, a, y_latchy, y_clean);
    a = 8'hFF; #1;
    $display("  %b   %h |    %h       %h   <- input changed, latchy did not", sel, a, y_latchy, y_clean);
    $finish;
  end
endmodule
sel  a  | y_latchy y_clean
  1   a5 |    a5       a5
  0   3c |    a5       00   <- latchy remembers stale a5
  0   ff |    a5       00   <- input changed, latchy did not
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmprgvn7cje/latch_demo.v:43: $finish called at 3 (1s)

Once sel drops to 0, y_latchy is frozen at the stale value a5 no matter what the input does, while y_clean follows its else branch. That frozen behaviour is a latch. Three reliable ways to avoid it:

  1. Complete every if with an else (and every case with a default) in combinational blocks.
  2. Assign defaults at the top of the block, then let the branches override. This scales far better than matching every branch when a block drives several outputs.
  3. Read the synthesis log. Any line mentioning “latch inferred” in a block you intended to be combinational is a bug, not a note.

A subtler variant of the same bug: assigning some outputs in one branch and other outputs in another. The paths all exist, but no single output is covered on all of them — each partially covered signal gets its own latch.

if-else in Sequential Logic

Inside a clocked always block the rules change. A flip-flop holds its value by itself, so a missing else is not only safe, it is the standard way to describe an enable. The branch order now encodes control priority — reset first, then enable, then everything else:

// count4.v
module count4 (
  input  wire       clk,
  input  wire       rst_n,
  input  wire       en,
  output reg  [3:0] count
);
  always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
      count <= 4'd0;        // reset checked first: highest priority
    else if (en)
      count <= count + 4'd1;
    // no final else: a flip-flop holds its value on its own
  end
endmodule

// Testbench
module tb_count4;
  reg  clk, rst_n, en;
  wire [3:0] count;

  count4 dut (.clk(clk), .rst_n(rst_n), .en(en), .count(count));

  always #5 clk = ~clk;

  initial begin
    $monitor("t=%0t  rst_n=%b en=%b count=%0d", $time, rst_n, en, count);
    clk = 0; rst_n = 0; en = 0;
    #12 rst_n = 1;     // release reset
    #10 en   = 1;      // count for three clocks
    #30 en   = 0;      // pause: count holds without any else
    #20 en   = 1;      // resume
    #13 rst_n = 0;     // async reset wins even while en=1
    #10 $finish;
  end
endmodule
t=0  rst_n=0 en=0 count=0
t=12  rst_n=1 en=0 count=0
t=22  rst_n=1 en=1 count=0
t=25  rst_n=1 en=1 count=1
t=35  rst_n=1 en=1 count=2
t=45  rst_n=1 en=1 count=3
t=52  rst_n=1 en=0 count=3
t=72  rst_n=1 en=1 count=3
t=75  rst_n=1 en=1 count=4
t=85  rst_n=0 en=1 count=0
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp27eb919m/count4.v:34: $finish called at 95 (1s)

Trace the log: while en is low the counter holds at 3 with no else written anywhere, and at t=85 the asynchronous reset clears it to 0 even though en is still high — because !rst_n is the first condition in the chain. Writing the reset check first is not a style preference; it is how the synthesizer recognizes the reset pin of the flop. Note also the non-blocking assignments (<=): use them for clocked logic, and blocking (=) for combinational blocks.

if-else vs case: Choosing the Right Tool

Both describe multi-way selection, but they say different things to the synthesizer:

if-else-ifcase
ConditionsAny independent expressions (a > b, err && !busy)One expression compared against constant labels
Hardware shapeCascaded 2:1 muxes — priority chainTypically a single wide parallel mux
TimingDelay grows with each branchFlat; friendlier for many alternatives
Natural fitPriority: resets, arbiters, exception-first checks, range comparisonsDecoders, ALU opcode select, FSM state registers
Latch riskMissing else in combinational logicMissing default or unlisted labels

A practical rule of thumb: if you are testing one signal against constants, write a case; if you are testing different conditions where order matters, write if-else-if. When the conditions happen to be mutually exclusive — say, a one-hot select — a long if-else-if chain still builds priority hardware the design does not need, so the case version is both clearer and faster. We cover case, casez and casex in detail in the next chapter.

Checklist Before You Move On

  • Use if-else only inside procedural blocks; conditions evaluating to x/z take the else path.
  • if-else-if order defines hardware priority — put the branch that must win first.
  • Combinational block: assign every output on every path (full else coverage or defaults at the top), or you get an inferred latch.
  • Sequential block: missing else means “hold”, which is exactly what a flop does; reset goes in the first branch.
  • Many unrelated conditions in order → if-else-if; one variable against constants → case.

Quick check

In a combinational always block, if (en) y = d; has no else. What hardware results and why?

A level-sensitive latch on y. When en is 0 no assignment executes, so y must retain its previous value — and retaining a value in combinational logic requires a storage element. Fix it by adding an else or assigning a default to y at the top of the block.

In an if-else-if chain, which condition wins when several are true at once?

The first (topmost) true condition. Later conditions are only evaluated when every earlier one is false, so the chain implements priority logic, and the branch order is part of the design intent.

Why is a missing final else acceptable in a clocked always block but not in a combinational one?

A flip-flop inherently holds its value between clock edges, so “no assignment this cycle” simply means the register keeps its state — no extra hardware is created. In a combinational block there is no storage, so an uncovered path forces the tool to infer a latch to provide it.

What does Verilog do when an if condition evaluates to x or z?

The condition is treated as false and the else branch (if present) executes. Real gates carry only 0 or 1, so logic that depends on this behaviour can simulate differently from how the fabricated chip behaves — one reason to clean up x-propagation and keep === comparisons inside testbenches.

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