A common assumption about #0 is that it means "let everything else scheduled for this time step finish first, then run this." That's not quite what it does — and the gap between that assumption and what actually happens is a genuine, well-documented gotcha, directly explained by the region order established in SystemVerilog Scheduling Semantics.

11 min read | Intermediate Level

What You'll Learn

  • Which region #0 actually defers execution to, and why that's earlier than many people expect
  • Why a #0-delayed read can still see a stale value relative to a nonblocking assignment scheduled in the same time step
  • How to correctly wait for a signal's fully settled value instead
  • Why #0 is generally discouraged as a quick fix for a timing problem

What #0 Actually Does

A statement delayed by #0 is deferred from Active into the Inactive region — one step later in that same time step, not pushed out to some point after everything has settled. Critically, Inactive still runs before NBA. Anything relying on a nonblocking assignment's update to have already happened isn't safe inside a #0-delayed statement, because that update genuinely hasn't happened yet.

The Gotcha: #0 Still Runs Before NBA

Here are four different ways of reading the same signal, all triggered by the same clock edge, right after a nonblocking assignment updates it:


module tb;
  reg clk;
  reg [7:0] a;

  always #10 clk = ~clk;

  initial a = 0;

  // Updates a - the actual write is deferred to NBA
  always @ (posedge clk) begin
    a <= 5;
  end

  // Reads immediately, in Active
  always @ (posedge clk) begin
    $display ("[%0t] immediate read (Active):    a=%0d", $time, a);
  end

  // Reads after a #0 delay, in Inactive - still before NBA!
  always @ (posedge clk) begin
    #0;
    $display ("[%0t] #0-delayed read (Inactive): a=%0d", $time, a);
  end

  // Reads via $strobe, deferred to Postponed - after NBA has settled
  always @ (posedge clk) begin
    $strobe ("[%0t] strobe read (Postponed):    a=%0d", $time, a);
  end
endmodule

At the very first posedge, with a starting at 0: the immediate read executes in Active, before the nonblocking assignment's write has applied, and sees a=0. The #0-delayed read executes next, in Inactive — still before NBA — and also sees a=0, exactly the same stale value, despite having been deliberately delayed. Only the $strobe read, deferred all the way to Postponed, correctly reports a=5 once NBA has actually applied the update.

Quick Check

Q: If the #0-delayed block were changed to #1 instead of #0, would it then see the updated value?

Show Answer

Yes, but for a completely different reason: #1 is a nonzero delay, which advances simulation to an entirely new time step — one full clock period later in this example, not just a later region within the same time step. By then, the earlier time step's NBA update has long since applied. This isn't the same mechanism as #0 at all.

Correctly Waiting for a Settled Value

As the example shows, the reliable way to read a value after all of a time step's updates have applied is $strobe or $monitor — both execute in Postponed, guaranteed to run after every other region, including NBA, has finished. Waiting for the following clock edge works too, since by then the previous edge's NBA updates have definitely settled. #0 accomplishes neither of these.

Where Does This Matter in Practice?

#0 is sometimes used deliberately for narrow, specific purposes — like controlling relative ordering between multiple Active-region events within the same time step. It should not be reached for as a general-purpose fix for a suspected race condition, since it doesn't resolve the specific class of race that involves a nonblocking assignment — exactly the case demonstrated in this article.

Common Beginner Mistakes

Mistake #1: Assuming #0 waits for nonblocking updates to settle

As this article's example shows, it doesn't — #0 only defers past Active into Inactive, which still runs before NBA. Why? A signal updated by a nonblocking assignment in the same time step is still not yet updated when a #0-delayed statement runs, no matter how intuitive it might seem that the delay should have let it settle.

Mistake #2: Reaching for #0 as a generic race-condition fix

#0 can resolve some ordering questions within Active, but not the specific, very common case of racing against a nonblocking assignment. Why? Using it without understanding exactly which region it lands in risks "fixing" the symptom on one simulator while leaving the underlying issue intact, or fully unresolved, on another.

Mistake #3: Confusing #0 with a nonzero delay like #1

These are fundamentally different mechanisms, as the Quick Check above shows. Why? #0 stays within the same time step, just in a later region; any nonzero delay advances to a genuinely new, later time step entirely — conflating the two leads to incorrect assumptions about what each one actually guarantees.

Mistake #4: Using $display instead of $strobe when a settled value is needed

As this article's example shows, a plain $display in Active can show exactly the same stale value a #0-delayed read would. Why? If a check specifically needs the fully settled value for a time step, $strobe or $monitor is the tool built for that — neither $display nor #0 provides that guarantee.

Practice Exercise

Challenge: Trace a Different Starting Value

Using the same four-block structure from this article, but with a starting at 10 and the nonblocking assignment changed to a <= a + 1;, predict what each of the four reads would show at the first clock edge.

Hint

Work out which value each read is comparing against — the pre-edge value of a, or the value after the nonblocking assignment has actually applied — using the same region ordering traced in the article.

Solution

The immediate (Active) read and the #0-delayed (Inactive) read both show a=10 — the pre-edge value, since neither runs after NBA. The $strobe (Postponed) read shows a=11, correctly reflecting a <= a + 1 once NBA has applied it. Same pattern as the article's own example, just with different numbers.

Summary

In this tutorial, you learned:

  • #0 defers a statement from Active into Inactive — a later region within the same time step, but still strictly before NBA
  • A #0-delayed read can see exactly the same stale value an immediate read would, if it's racing against a nonblocking assignment in the same time step
  • $strobe and $monitor, running in Postponed, are the reliable way to see a time step's fully settled value
  • #0 is a same-time-step mechanism, fundamentally different from any nonzero delay, which advances to a genuinely new time step
  • #0 should not be treated as a general-purpose fix for a suspected race — it doesn't resolve races involving nonblocking assignments
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5