Immediate assertions are executed based on simulation event semantics and are required to be specified in a procedural block. They're treated the same way as the expression in an if statement during simulation.

The immediate assertion passes if the expression holds true at the time the statement executes, and fails if the expression evaluates to false (X, Z, or 0). These assertions are intended for use in simulation and aren't suitable for formal verification. They can be used in both RTL design and testbench code to flag errors during simulation.

10 min read | Intermediate Level

What You'll Learn

  • What makes an assertion "immediate" rather than concurrent, and where it can be used
  • The three ways to write an immediate assertion: bare, with pass/fail blocks, and with a name label
  • How an immediate assertion inside a design catches a protocol violation the instant it happens
  • Why wrapping a randomize() call in an assertion turns an easily-missed warning into an unmissable failure

Syntax


// Simple assert statement
assert();

// Assert statement with statements to be executed for pass/fail conditions
assert() begin
	// If condition is true, execute these statements
end else begin
	// If condition is false, execute these statements
end

// Optionally give name for the assertion
[assert_name] : assert();

Immediate Assertion in Design

Here's an example where the design has an immediate assertion checking that a push request to the FIFO doesn't come at a time when the FIFO is already full. If the expression within the assert statement evaluates to true, the first begin end block executes; if false, the else part executes. This is much like the if construct, with the difference that it isn't required for the user to place a display statement to flag the error — the assertion mechanism does that automatically:


module my_des (my_if _if);
  
  always @ (posedge _if.clk) begin
    if (_if.push) begin
    	// Immediate assertion and ensures that
    	// fifo is not full when push is 1
    	a_push: assert (!_if.full) begin
      		$display("[PASS] push when fifo not full");
    	end else begin
      		$display("[FAIL] push when fifo full !");
    	end
  	end
  
    if (_if.pop) begin
    	// Immediate assertion to ensure that fifo is not
    	// empty when pop is 1
    	a_pop: assert (!_if.empty) begin
      		$display ("[PASS] pop when fifo not empty");
    	end else begin
      		$display ("[FAIL] pop when fifo empty !");
    	end
    end
  end
endmodule

Without immediate assertions like this, achieving the same check with concurrent assertions would mean re-expressing the "only when push is asserted" condition from scratch — the immediate assertion instead reuses the surrounding if (_if.push) that already got you to exactly the right point in the procedural flow, at no extra cost.


interface my_if(input bit clk);
  logic pop;
  logic push;
  logic empty;
  logic full;
endinterface

module tb;
  bit clk;
  always #10 clk <= ~clk;
  
  my_if _if (clk);  
  my_des u0 (.*);
  
  initial begin
    for (int i = 0; i < 5; i++) begin
      _if.push  <= $random;
      _if.pop   <= $random;
      _if.empty <= $random;
      _if.full  <= $random;
      $strobe("[%0t] push=%0b full=%0b pop=%0b empty=%0b",
              $time, _if.push, _if.full, _if.pop, _if.empty);
      @(posedge clk);
    end
    #10 $finish;
  end  
endmodule

Notice how the time and line of assertion failure is displayed as E:

Output
ncsim> run
[0] push=0 full=1 pop=1 empty=1
ncsim: *E,ASRTST (./design.sv,13): (time 10 NS) Assertion tb.u0.a_pop has failed
[FAIL] pop when fifo empty !
[10] push=1 full=0 pop=1 empty=1
[PASS] push when fifo not full
ncsim: *E,ASRTST (./design.sv,13): (time 30 NS) Assertion tb.u0.a_pop has failed
[FAIL] pop when fifo empty !
[30] push=1 full=1 pop=1 empty=0
ncsim: *E,ASRTST (./design.sv,5): (time 50 NS) Assertion tb.u0.a_push has failed
[FAIL] push when fifo full !
[PASS] pop when fifo not empty
[50] push=1 full=0 pop=0 empty=1
[PASS] push when fifo not full
[70] push=1 full=1 pop=0 empty=1
ncsim: *E,ASRTST (./design.sv,5): (time 90 NS) Assertion tb.u0.a_push has failed
[FAIL] push when fifo full !
Simulation complete via $finish(1) at time 100 NS + 0
./testbench.sv:25     #10 $finish;
ncsim> exit

At the very first posedge (10ns), the values driven at 0ns are what get checked: pop was 1 and empty was 1, so a_pop fails immediately — push was 0 at that same moment, so a_push's if (push) never even evaluates that cycle.

Quick Check

Q: At 50ns, a_push fails while a_pop passes in the same clock cycle. Why can these two outcomes differ within one edge?

Show Answer

They're two completely independent checks against different signal pairs — push against full, and pop against empty. Nothing links their outcomes together; the random stimulus at that specific moment simply happened to violate one condition while satisfying the other.

Immediate Assertion in Testbench

Assume a class called Packet is created and randomized, but this example has a constraint error, and randomization will fail. Without an assertion, the failure is only displayed as a warning message — if the user isn't careful, the test may display incorrect behavior and may even appear to pass:


class Packet;
  rand bit [7:0] addr;
  
  constraint c_addr { addr > 5; addr < 3; }
endclass

module tb;
  initial begin
    Packet m_pkt = new();
  
    m_pkt.randomize();
  end
endmodule

Output

Output
ncsim> run
    m_pkt.randomize();
                  |
ncsim: *W,SVRNDF (./testbench.sv,11|18): The randomize method call failed. The unique id of the failed randomize call is 0.
Observed simulation time : 0 FS + 0
ncsim: *W,RNDOCS: These constraints contribute to the set of conflicting constraints:

  constraint c_addr { addr > 5; addr < 3; } (./testbench.sv,4)
ncsim: *W,RNDOCS: These variables contribute to the set of conflicting constraints:

rand variables:
       addr [./testbench.sv, 2]

ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

Notice that c_addr itself asks for an impossible value — addr can never be both greater than 5 and less than 3 at once — so this call fails every single time, yet the only sign of it is a warning easy to scroll straight past.

Instead, an immediate assertion can be placed directly on the randomization method call, ensuring the return value is always 1, indicating success. If the assertion fails, it forces the user to actually look at the failure, reducing debug effort:


class Packet;
  rand bit [7:0] addr;
  
  constraint c_addr { addr > 5; addr < 3; }
endclass

module tb;
  initial begin
    Packet m_pkt = new();
  
    assert(m_pkt.randomize());
  end
endmodule

The simulator assigns a generated name for the assertion if the user hasn't specified one:

Output
ncsim> run
    assert(m_pkt.randomize());
                         |
ncsim: *W,SVRNDF (./testbench.sv,11|25): The randomize method call failed. The unique id of the failed randomize call is 0.
Observed simulation time : 0 FS + 0
ncsim: *W,RNDOCS: These constraints contribute to the set of conflicting constraints:

  constraint c_addr { addr > 5; addr < 3; } (./testbench.sv,4)
ncsim: *W,RNDOCS: These variables contribute to the set of conflicting constraints:

rand variables:
       addr [./testbench.sv, 2]

ncsim: *E,ASRTST (./testbench.sv,11): (time 0 FS) Assertion tb.unmblk1.__assert_1 has failed
ncsim: *W,RNQUIE: Simulation is complete.
ncsim> exit

The exact same underlying failure now produces a hard ASRTST assertion failure alongside the warning, instead of just the easily-missed warning on its own. In a similar way, assert can be used with any expression that evaluates to true or false within a procedural block.

Quick Check

Q: If the constraint were fixed to something satisfiable, like addr > 5; addr < 10;, would assert(m_pkt.randomize()); ever fail?

Show Answer

No. With a satisfiable constraint, randomize() succeeds and returns 1 every time, which the assertion accepts — the assertion only exists to catch a failure, not to introduce one on its own.

By default, a failed immediate assertion doesn't stop simulation on its own — both examples above continue running normally after each reported failure. This is because an assertion with no explicit severity system task defaults to $error severity, which reports but doesn't halt. Terminating on failure requires explicitly using $fatal in the assertion's else clause instead.

Where Is This Used?

Wrapping every randomize() call in an assertion (often through a team-defined shorthand macro) is considered mandatory practice in most production testbenches. An unnoticed randomization failure can silently leave a variable at its default or previous value instead of the intended randomized stimulus — potentially masking a real bug or producing a false pass with no obvious sign anything went wrong.

Common Beginner Mistakes

Mistake #1: Assuming immediate assertions work for formal verification

Immediate assertions are simulation-only. Why? They follow simulation event semantics tied to procedural execution order, which formal tools have no equivalent concept of — formal verification needs concurrent assertions instead.

Mistake #2: Trying to use an immediate assertion outside a procedural block

An immediate assertion has to be inside an always, initial, or similar procedural block. Why? It's evaluated exactly like an if statement at the point it's reached, so it needs the same procedural context an if statement would.

Mistake #3: Forgetting to wrap a randomize() call in an assertion

As the Packet example shows, an unwrapped randomize() failure only produces a warning that's easy to overlook. Why? A test can silently continue running with unintended data after a failed randomization, producing confusing or outright wrong results with no obvious explanation.

Mistake #4: Assuming a failed assertion always stops simulation

As the note above explains, the default severity only reports the failure — it doesn't halt anything. Why? If you specifically need simulation to stop the moment a check fails, that requires an explicit $fatal call in the assertion's else clause, not just the bare assertion itself.

Practice Exercise

Challenge: Check Mutual Exclusion

Write an immediate assertion inside an always @(posedge clk) block that checks wr_en and rd_en are never both high at the same time. Name the assertion a_mutex, and print "[FAIL] wr_en and rd_en both high!" on failure.

Hint

The condition you want to hold true is that at most one of the two signals is high — not both. Express that as a single boolean expression inside the assert(...).

Solution

always @ (posedge clk) begin
  a_mutex: assert (!(wr_en && rd_en)) begin
    // Passed - nothing to do
  end else begin
    $display ("[FAIL] wr_en and rd_en both high!");
  end
end

!(wr_en && rd_en) is true whenever at most one of the two signals is high, and false only when both are high at once — exactly the mutual exclusion condition being checked.

Summary

In this tutorial, you learned:

  • An immediate assertion is evaluated like an if statement, at the exact point it's reached in a procedural block
  • Immediate assertions are simulation-only, and can be used in both design and testbench code
  • Placing an immediate assertion inside existing control flow (like if (push)) reuses that condition instead of re-expressing it separately, unlike a concurrent assertion
  • Wrapping a randomize() call in an assertion turns an easily-missed warning into an unmissable failure
  • A failed assertion doesn't stop simulation by default — that requires explicitly using $fatal severity instead
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5