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
endThree rules matter here:
- The condition is any expression. Verilog treats a non-zero value as true.
if (count)is legal and meansif (count != 0). - Unknowns count as false. If the condition evaluates to
xorz, theelsebranch runs. This is a frequent source of simulation-vs-silicon mismatch, because real gates never carry anx. Keep===comparisons (which matchxliterally) for testbenches only. - Use
begin...endfor more than one statement. Without it, only the first statement belongs to the branch. Many teams requirebegin...endon 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
endmodulereq | 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 ifadds a mux level, so a 16-way chain creates a 16-deep path that can wreck timing. If the conditions are mutually exclusive anyway, acasestatement 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;
endKeep 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
endmodulesel 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:
- Complete every
ifwith anelse(and everycasewith adefault) in combinational blocks. - 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.
- 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
endmodulet=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-if | case | |
|---|---|---|
| Conditions | Any independent expressions (a > b, err && !busy) | One expression compared against constant labels |
| Hardware shape | Cascaded 2:1 muxes — priority chain | Typically a single wide parallel mux |
| Timing | Delay grows with each branch | Flat; friendlier for many alternatives |
| Natural fit | Priority: resets, arbiters, exception-first checks, range comparisons | Decoders, ALU opcode select, FSM state registers |
| Latch risk | Missing else in combinational logic | Missing 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-elseonly inside procedural blocks; conditions evaluating tox/ztake theelsepath. if-else-iforder defines hardware priority — put the branch that must win first.- Combinational block: assign every output on every path (full
elsecoverage or defaults at the top), or you get an inferred latch. - Sequential block: missing
elsemeans “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.
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