Combinational RTL
Avoiding Accidental Latches in Verilog
How always @(*) describes combinational hardware, what an inferred latch is, the four coding patterns that cause one, and the two habits that prevent every case.
5 min read · Updated
An accidental latch is one of the first hardware surprises that catches beginners writing combinational Verilog. The code looks fine, the simulation seems to work, and then a synthesis warning appears: Inferred latch on signal .... This guide explains what that means, why it happens, and the two habits that make it impossible.
What always @(*) actually describes
always @(*) is the conventional classic-Verilog form for an intended combinational procedural process. The simulator automatically includes signals read by the block in its sensitivity set and re-evaluates the block when one changes. Statements inside each evaluation still execute in procedural order.
The (*) prevents a stale, hand-written sensitivity list; it does not guarantee combinational hardware. Complete assignment behavior is what matters. If a procedurally assigned variable lacks a value on some path, the design requires that value to remember its previous state, and synthesis can infer a latch.
That continuous-reaction model is exactly what makes the next rule non-negotiable.
What is a latch, and why do you get one by accident?
A latch is a small storage element. It holds its output at the last value it was given until something tells it to change. That is useful when you want memory — but inside a block you intended to be pure combinational logic, it is almost never what you want.
The rule that causes accidental latches is simple: an intended combinational block must give every procedurally assigned variable a value on every possible path through the code. That includes intermediate procedural variables as well as module outputs. If a path leaves one unchanged, the synthesizer must preserve its old value to match the RTL behavior. That preservation requires storage: the latch you did not intend.
Inside always @(*), statements are evaluated in procedural order, but synthesis interprets the block's complete behavior as hardware. "This output is sometimes not assigned" translates directly into "this output sometimes needs to remember," and remembering means a latch.
The four ways a latch sneaks in
Every accidental latch has the same root cause — an unassigned path — but it shows up in four recognizable shapes.
1. Missing else. An if with no else leaves the output untouched when the condition is false:
always @(*) begin
if (enable)
y = data;
// when enable is 0, y is never assigned -> latch
end2. Missing default. A case that does not list every possible value and has no default leaves the uncovered values unassigned:
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
// 2'b10 and 2'b11 are unassigned -> latch
endcase
end3. Partial case assignment. Some branches assign the output and others forget. The forgetful branches are the unassigned paths.
4. Unassigned branch in a larger block. In a block with several outputs, one path assigns some of them and never touches another. That untouched output latches.
The two habits that prevent all four
You do not need to memorize four fixes. Two habits close every case.
Default-at-top. Assign every procedural result and temporary a safe default before any if or case that might override it:
always @(*) begin
y = 1'b0; // default at top
if (enable)
y = data; // override only when needed
endBecause y already has a value before any branch runs, there is no path where it is unassigned — so no latch is ever inferred. This is the pattern this course uses from the first always @(*) lesson onward, precisely so latch avoidance becomes a habit rather than a rule you remember under pressure.
Assign every procedural value in every branch. The alternative is to make sure each branch — every if/else arm, every case item plus a default — assigns every relevant output and intermediate variable explicitly. This is more verbose but equally correct.
Most designers reach for default-at-top because it is shorter and harder to get wrong: you cannot forget a branch that does not exist yet.
Predict before running
The example below puts a missing-else block (maybe_latch) next to its default-at-top fix (no_latch), driven by the same enable/data inputs. It runs three steps: enable high, then enable low with the same data, then enable low with new data.
Before running it, predict what y_latch does on that third line, after data changes while enable is still low:
- does
y_latchfollow the newdatavalue, or does it keep showing the old one? - what does
y_fixedshow at that same moment?
Expected output:
enable=1 data=1 y_latch=1 y_fixed=1
enable=0 data=1 y_latch=1 y_fixed=0
enable=0 data=0 y_latch=1 y_fixed=0That third line is the bug, caught live: data changed to 0, but y_latch still reads 1 — the value it held from the first line. maybe_latch's always @(*) block re-evaluates on every data change, but with enable low there is no assignment for y to execute, so simulation (and synthesis) must remember the last value. y_fixed has no such gap: every evaluation starts from the same default, so it correctly shows 0 — "no valid source selected" — instead of quietly replaying stale data.
How to react to the warning
When you see Inferred latch on signal X, do not ignore it. Find the block that assigns X, and look for the path where X gets no value. Add a default at the top of that block, or complete the missing branch. Re-check, and the warning disappears — along with the phantom storage it was describing.
Reading and fixing that warning quickly is a real signal of combinational-Verilog fluency. It is the difference between "my simulation passed" and "my hardware does what I meant."
Sources and verification
Example provenance: SkillLift Labs authored the example for this tutorial. tests/scripts/tutorial-verified-examples.test.ts compiles it in Verilog-2005 mode, reproduces the published output, and independently checks the latch and complete-assignment behavior. The same test exercises the article-to-runner source split. Automation verifies those stated properties; human technical review for indexing remains a separate gate. Try the same comparison with Run this example inline above—it runs the exact code shown.