@event Vs wait(event.triggered) in SystemVerilog
In short: Both @(e) and wait(e.triggered) pause a process until event e fires, but they treat timing differently. @(e) is edge-sensitive: the process must already be blocked on it before the trigger, or it misses the event. wait(e.triggered) is level-sensitive within the current time step: it still catches the event even if the trigger happened an instant earlier in the same time slot. Use wait(e.triggered) when a race between the trigger and the wait is possible.
SystemVerilog gives two ways to wait for a named event. The difference is subtle but causes real bugs. @(e) only unblocks on a fresh trigger that happens after the process is already waiting. wait(e.triggered) checks a flag that stays true for the rest of the current time step, so the order of the trigger and the wait within the same step no longer matters. This is the classic fix for a “missed event” race.
A simple way to picture it
Imagine waiting for a doorbell. @(e) is like listening at the door: you only hear the bell if you are already listening at the exact moment it rings. If you walk up a second too late, you missed it and must wait for the next ring. wait(e.triggered) is like a bell that also lights a lamp that stays on for the rest of the minute. Even if you arrive just after it rang, the lamp is still lit, so you know it happened and you go in.
The core difference in one look
| Aspect | @(e) | wait(e.triggered) |
|---|---|---|
| Sensitivity | Edge: needs a new trigger while blocked | Level within the current time step |
| Order of trigger vs wait | Must wait first, then trigger | Either order works in the same step |
| Race safety | Can miss the event | Catches the event in the same step |
| Still needs same time step? | Yes | Yes |
| Typical use | When the wait is guaranteed to run first | When trigger and wait may race |
The missed-event race with @(e)
Here the trigger runs before the wait, both at time 0. Because @(e) is edge-sensitive, the process was not yet blocked when the trigger fired, so it misses it and the timeout branch wins.
class ping;
event e;
task trigger();
->e; // fires the event now
endtask
task wait_edge();
@(e); // edge-sensitive: must be blocked first
$display("[%0t] caught with @(e)", $time);
endtask
task run();
trigger(); // trigger happens BEFORE the wait
wait_edge();
endtask
endclass
module tb;
initial begin
ping p = new();
fork
p.run();
begin #10 $display("[%0t] missed the event", $time); end
join_any
disable fork;
end
endmoduleThe event is triggered and gone before @(e) starts listening, so the “missed the event” line prints at time 10. The @(e) would wait forever otherwise.
The fix with wait(e.triggered)
Swap the edge wait for wait(e.triggered). The triggered flag is set for the whole current time step, so even though the trigger ran first, the wait still sees it and unblocks immediately.
class ping;
event e;
task trigger();
->e;
endtask
task wait_level();
wait(e.triggered); // level-sensitive in this time step
$display("[%0t] caught with wait(e.triggered)", $time);
endtask
task run();
trigger(); // trigger still happens first
wait_level();
endtask
endclass
module tb;
initial begin
ping p = new();
fork
p.run();
begin #10 $display("[%0t] missed the event", $time); end
join_any
disable fork;
end
endmoduleNow the “caught with wait(e.triggered)” line prints at time 0, the timeout never fires, and the race is gone.
Blocking vs non-blocking trigger
There are also two ways to fire an event. ->e is the blocking trigger; it takes effect in the active region of the current step. ->>e is the non-blocking trigger; it schedules the trigger for the non-blocking region of the same step, a little later. The non-blocking form is useful when you want the waiters that are already blocked to be the ones that see it, avoiding some same-step ordering surprises.
event blocking_e;
event nonblk_e;
initial begin
-> blocking_e; // blocking trigger: active region
->> nonblk_e; // non-blocking trigger: NBA region, slightly later
endIn practice, reach for wait(e.triggered) to make a waiter race-safe, and consider ->> when you need the trigger to land after processes have had a chance to block in the same step.
Which one should you use?
- Use
@(e)when you can guarantee the waiting process is already blocked before any trigger, for example a monitor that sits on the event from time 0. - Use
wait(e.triggered)whenever the trigger and the wait could happen in either order in the same time step. This is the safe default for handshakes between parallel processes. - Consider
->>(non-blocking trigger) when you want the trigger to take effect after waiters have blocked in the same step.
Expected behaviour, in plain words
In the first example the trigger fires before @(e) is listening, so the event is missed and the timeout prints “missed the event” at time 10. In the second example wait(e.triggered) sees the flag set in the same time step and prints its “caught” line at time 0, so the timeout never runs. I review this against the language rules rather than run it, so confirm the exact timestamps in your own simulator or on EDA Playground.
Common mistakes to avoid
- Using
@(e)across a race. If the trigger can fire before the wait blocks, the event is lost. Usewait(e.triggered)instead. - Assuming
wait(e.triggered)works across time steps. The triggered flag is only set for the current time step; you still must reach the wait in the same step. - Confusing the two triggers.
->is blocking (active region),->>is non-blocking (NBA region). They land at slightly different points in the step. - Triggering before any process is listening with
@. A single unmatched trigger simply vanishes. - Relying on ordering between parallel threads. Do not assume which of two forked threads runs first; design for either order.
Keep learning
Events are one way threads talk to each other. Compare them with the constructs in fork join in SystemVerilog, and see class basics in encapsulation in SystemVerilog. For synchronization inside a testbench, read the UVM testbench architecture. More is in our SystemVerilog guides and interview questions.
Frequently asked questions
What is the difference between @(e) and wait(e.triggered)?
@(e) is edge-sensitive: the process must already be blocked on it before the event fires, or it misses the trigger. wait(e.triggered) is level-sensitive within the current time step, so it catches the event even if the trigger happened an instant earlier in the same step.
Why does @(e) sometimes miss an event?
Because it only reacts to a trigger that occurs while the process is already waiting. If the trigger fires before the process reaches @(e), even in the same time step, there is nothing to react to and the event is lost.
How does wait(e.triggered) avoid the race?
When an event is triggered, its triggered property stays true for the rest of that time step. wait(e.triggered) checks that flag, so it succeeds whether the trigger came just before or just after the wait, as long as both are in the same step.
Does wait(e.triggered) work across different time steps?
No. The triggered flag is only set for the current time step. The waiting process must execute the wait in the same time step as the trigger to catch it.
What is the difference between -> and ->> for triggering?
-> is a blocking trigger that takes effect in the active region of the current time step. ->> is a non-blocking trigger that schedules the event in the non-blocking region of the same step, so it lands slightly later.
When should I prefer @(e) over wait(e.triggered)?
Use @(e) when you can guarantee the waiting process is already blocked before any trigger, such as a monitor that waits from the start. When the trigger and wait can happen in either order, wait(e.triggered) is the safer choice.



