← All guides

The always_comb latch error, explained

Debugging a specific message? You’re in the right place. For the underlying concept, see inferred latches in Verilog or in VHDL.

If synthesis just stopped with something like:

ERROR: Latch inferred for signal `\grant.\grant_out' from always_comb process

or, in friendlier tools:

Latch inferred for 'grant_out': always_comb requires every output to be
assigned in every branch. Add an else or a default assignment at the top
of the block (or use always_latch if a latch is intended).

then your always_comb block has a path through it that doesn’t assign one of its outputs, and the tool is refusing to do what plain Verilog would have done silently: build a level-sensitive latch to remember the old value.

This error is a feature. always_comb isn’t just a shorter spelling of always @(*). It’s a promise to the tools that the block describes purely combinational logic. When the code breaks that promise, the tool errors instead of quietly inserting storage you didn’t ask for.

Watch the difference

This block uses old-style always @(*), so the missing else silently becomes a latch; you can see it in the schematic. Now change always @(*) to always_comb and press Run: the same code becomes a hard error, with the message pointing at the block:

Interactive schematic by RapidRTL

Same hardware question, two philosophies: @(*) assumes you meant it; always_comb makes you say what you meant.

The fixes

The error names a signal; make sure that signal is assigned on every path through the block. Either:

always_comb begin
  grant_out = 1'b0;        // default at the top: every path now assigns
  if (enable)
    grant_out = req;
end

or complete the branch structure (else, default: in every case). The full menu of idioms is in the inferred-latches guide.

If the latch is intentional, say that too. That’s what always_latch is for:

always_latch begin
  if (enable)
    q <= d;                // deliberately a latch; tools now expect one
end

The intent family, in one table

BlockPromise to the toolsBroken promise becomes
always_combpurely combinationalhard error (this page)
always_ff @(posedge clk)clocked registers onlyerror if no storage is implied
always_latchlevel-sensitive latcherror if no latch is inferred
always @(*)none (legacy Verilog-2001)silent latch, maybe a warning

If your codebase still uses always @(*), the cheapest lint rule you will ever adopt is replacing it with always_comb: this whole class of bug turns from a warning someone ignores into an error nobody can.

Why did it work in simulation?

Simulators happily model the latch: the block simply keeps the old value, and your testbench may never toggle the inputs in the order that exposes it. That’s the trap. The code looks verified, and synthesis (or always_comb) is where the promise gets checked against the hardware.