Skip to content

SystemVerilog · Module 12

Assertion Severity & Action Blocks

The four severity system tasks — $fatal, $error, $warning, $info — and the action-block syntax that runs them. How severity choice shapes regression behaviour, the pass-action vs fail-action distinction, custom message formatting, and why matching severity to spec criticality is one of the most consequential SVA-discipline decisions.

Module 12 · Page 12.11

What Action Blocks Are

Every SVA assertion construct from Module 12.10 (assert property, assume property, cover property) supports two optional action blocks — code that runs when the assertion passes or fails. The action blocks are where the assertion's consequences are encoded: what message gets logged, what severity level is reported, whether the simulation stops, what diagnostic data is captured.

The action-block syntax is:

SystemVerilog — action-block syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assert property (p)
    pass_action               // runs when property is true
else
    fail_action;              // runs when property is false

Both action blocks are optional. The fail action is the canonical form — what runs when a contract is violated. The pass action is much less common; production codebases use it for $info-based regression tracing or coverage helpers.

Modules 12.2 and 12.4 covered immediate assertions and concurrent assertions briefly using $error as the fail action. This page is the deep dive on the four severity system tasks, their semantics, message-formatting discipline, and the production patterns that turn a 500-assertion regression log into actionable verification evidence.

The Four Severity System Tasks

The four severity tasks are SystemVerilog system functions ($fatal, $error, $warning, $info) that the simulator handles specially when called from inside an assertion's action block. Each task has a defined effect on simulation flow and a defined effect on the regression's failure-count tallies.

$fatal — Immediate Stop

SystemVerilog — fatal action
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_counter_overflow:
assert property (@(posedge clk) disable iff (rst)
    count <= COUNT_MAX)
    else $fatal(1, "Counter overflow: count=%0d > MAX=%0d — impossible state",
                  count, COUNT_MAX);
 
// Arguments to $fatal:
//   First argument: finish code (0 = clean, 1 = error, 2 = no message)
//   Subsequent arguments: format string + interpolated values
//
// Effect:
//   Simulation stops IMMEDIATELY at this assertion failure.
//   Other assertions in flight for the same time step do NOT fire.
//   Output buffers flush before exit.

When to use $fatal. Only when continuing the simulation cannot produce meaningful results. Counter overflowed its width, pointer to an unallocated object, configuration parameter outside the synthesisable range. The "this should never happen physically" cases.

The cost of $fatal. The first failing assertion stops the simulation. Any other bugs in the same run are never reported. A regression with five real bugs that all use $fatal becomes five sequential regression runs — find bug 1, fix it, re-run; find bug 2; etc. — turning a 1-hour batch into a 5-hour serial chase.

$error — Continue and Count

SystemVerilog — error action
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_protocol_violation:
assert property (@(posedge clk) disable iff (rst)
    req |=> ack)
    else $error("[%0t] req-ack handshake violation — req=%b ack=%b",
                  $time, req, ack);
 
// Effect:
//   Simulation continues.
//   The simulator's error counter increments.
//   The regression's pass/fail status is "FAIL" if any errors fired.
//   Other assertions continue to evaluate; multiple errors can fire
//   in the same run for triage.

$error is the canonical fail action. Production protocol-compliance assertions, FSM contract assertions, FIFO overflow/underflow assertions — all use $error. The continuation behaviour means a single regression run surfaces every assertion failure, not just the first.

The format-string convention: every fail message starts with [%0t] (the current simulation time) and includes the offending signal values. The message answers "where, when, and with what values" without the verification engineer needing to re-run with waveform capture.

$warning — Suspicious But Continue

SystemVerilog — warning action
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_latency_approaching_limit:
assert property (@(posedge clk) disable iff (rst)
    latency <= MAX_LATENCY)
    else $warning("[%0t] Latency=%0d approaching spec maximum of %0d",
                    $time, latency, MAX_LATENCY);
 
// Effect:
//   Simulation continues.
//   The simulator's warning counter increments.
//   The regression's pass/fail status is NOT affected by warnings.

When to use $warning. Conditions that are legal per spec but worth flagging — latency near the spec maximum, queue depth near capacity, a corner case that succeeded with some unusual timing. The conditions are not bugs but might indicate something worth investigating.

The misuse of $warning. Using $warning for real bugs lets the regression triage miss the failure — most CI systems gate on errors, not warnings, so the warning fires and the build still goes green. Production discipline reserves $warning for non-bug-but-noteworthy; spec violations use $error.

$info — Informational Tracing

SystemVerilog — info action
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_handshake_seen:
assert property (@(posedge clk) disable iff (rst)
    req |=> ack)
    $info("[%0t] req-ack handshake completed", $time)
    else $error("[%0t] req-ack violation", $time);
 
// $info is in the PASS-action position; it runs when the property is TRUE.
// (Modules 12.2 and 12.4 noted that pass actions are rare; this is one
//  of the legitimate uses.)

When to use $info. Debug tracing during interactive runs — confirming the regression is exercising specific scenarios, tracing FSM transitions, or producing a high-verbosity audit log. The $info task has no effect on regression pass/fail counts, so it's purely diagnostic.

$info in regression mode. Most CI flows suppress $info messages globally (via simulator command-line flags like VCS's +UVM_VERBOSITY=NONE or Questa's verbosity controls). The production pattern is "use $info for interactive-debug-only diagnostics; rely on cover properties for regression-time scenario tracking."

Action Blocks — Pass and Fail Positions

The action-block syntax has two positions: pass-action (between the closing paren and else) and fail-action (after else). Both are optional, and they can include any procedural statement — system tasks, function calls, variable assignments, conditionals.

SystemVerilog — action-block positions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Form 1 — fail-only (most common) ─────────────────────────────
assert property (p)
    else $error("...");
 
// ── Form 2 — pass + fail ─────────────────────────────────────────
assert property (p)
    $info("[%0t] handshake completed", $time)
    else $error("[%0t] handshake violation", $time);
 
// ── Form 3 — bare (no actions) ───────────────────────────────────
assert property (p);
// Simulator emits its default error message on failure.
 
// ── Form 4 — complex action with multiple statements ─────────────
int violation_count;
always @(posedge clk) begin
    assert property (p)
        else begin
            $error("[%0t] violation #%0d", $time, violation_count);
            violation_count++;
            if (violation_count > 100) $fatal(1, "Too many violations");
        end
end

The bare form (no actions) is occasionally useful for debug-time scratch code where the default message is informative enough. Production assertions almost always carry a fail action with $time and offending-signal context.

The compound-action form (Form 4) is useful for fail actions that need to perform multiple operations — increment counters, log to a file, escalate severity after N failures. Production discipline keeps action blocks small (one or two statements); complex logic belongs in a named task or function called from the action block.

Message Formatting — $sformatf and Format Specifiers

The fail message is the bug report a verification engineer reads at 11 PM the night before tape-out. Production discipline treats every fail message as a structured report containing time, signal values, and enough context to act without re-running the simulation.

SystemVerilog — message formatting patterns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Minimal — uninformative, do not use ─────────────────────────
a_minimal:
assert property (p)
    else $error("Assertion failed");
// Tells the engineer nothing they didn't know.
 
// ── Adequate — includes time and one signal ─────────────────────
a_adequate:
assert property (req |=> ack)
    else $error("[%0t] ack didn't follow req", $time);
 
// ── Production — time, named contract, all offending values ─────
a_production:
assert property (req |=> ack)
    else $error("[%0t] a_req_ack violation: req=%b ack=%b (expected ack=1 next cycle)",
                  $time, req, ack);
 
// ── Using $sformatf for complex formatting ──────────────────────
a_complex:
assert property (addr inside {[CSR_BASE:CSR_TOP]})
    else $error(
        $sformatf("[%0t] addr=0x%0h out of CSR range [0x%0h:0x%0h]",
                    $time, addr, CSR_BASE, CSR_TOP));

Format specifier conventions.

SpecifierUse for
%0dDecimal integer, no leading zeros
%0h (or %0x)Hex integer, no leading zeros
%0bBinary
%sString
%mThe hierarchical path of the assertion
%tTime (combined with $time for current sim time)

The [%0t] time-stamp convention is universal in production codebases — every fail message starts with the current simulation time so logs are sortable by $time for triage.

A Complete Working Example — Production-Shape Severity Discipline

The pattern below shows a property file that uses all four severity tasks deliberately, each matched to what the corresponding contract violation means.

SystemVerilog — APB property file with disciplined severity
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package apb_severity_discipline;
    default clocking @(posedge pclk); endclocking
    default disable iff (!presetn);
 
    // ── Property 1: PADDR within addressable range ──────────────
    property p_paddr_in_range(input bit [31:0] paddr);
        paddr inside {[CSR_BASE:CSR_TOP], [RAM_BASE:RAM_TOP], [MMIO_BASE:MMIO_TOP]};
    endproperty
 
    // ── Property 2: PSEL precedes PENABLE ───────────────────────
    property p_psel_before_penable(input bit psel, penable);
        $rose(penable) |-> $past(psel);
    endproperty
 
    // ── Property 3: PREADY arrives within 16 cycles ─────────────
    property p_pready_within_16(input bit psel, penable, pready);
        (psel && penable) |-> ##[0:15] pready;
    endproperty
 
    // ── Property 4: Slave-response latency under spec maximum ───
    property p_latency_under_target(input int latency);
        latency <= TARGET_LATENCY;
    endproperty
 
    // ── Property 5: Internal arbiter state machine in valid state
    property p_arbiter_state_legal(input bit [3:0] state);
        state inside {IDLE, BUSY, COMPLETE, ERROR};
    endproperty
endpackage
 
// ── Assertion module with severity-discipline applied ───────────
module apb_assertions (
    input  bit pclk, presetn,
    input  bit psel, penable, pready,
    input  bit [31:0] paddr,
    input  int  latency,
    input  bit [3:0] arbiter_state
);
    import apb_severity_discipline::*;
 
    // ── $fatal for impossible-state contracts ───────────────────
    a_arbiter_legal:
    assert property (p_arbiter_state_legal(arbiter_state))
        else $fatal(1,
            "[%0t] a_arbiter_legal: arbiter_state=0x%0h not in valid set "
            "{IDLE, BUSY, COMPLETE, ERROR} — design has corrupted state",
            $time, arbiter_state);
 
    // ── $error for protocol violations ─────────────────────────
    a_paddr_range:
    assert property (p_paddr_in_range(paddr))
        else $error("[%0t] a_paddr_range: paddr=0x%0h out of addressable range",
                      $time, paddr);
 
    a_psel_before_penable:
    assert property (p_psel_before_penable(psel, penable))
        else $error("[%0t] a_psel_before_penable: PENABLE rose without PSEL last cycle",
                      $time);
 
    a_pready_within_16:
    assert property (p_pready_within_16(psel, penable, pready))
        else $error("[%0t] a_pready_within_16: PREADY > 16 cycles after ACCESS",
                      $time);
 
    // ── $warning for legal-but-noteworthy conditions ───────────
    a_latency_under_target:
    assert property (p_latency_under_target(latency))
        else $warning("[%0t] a_latency_under_target: latency=%0d exceeds target=%0d "
                        "(still within spec maximum, but worth investigating)",
                        $time, latency, TARGET_LATENCY);
 
    // ── $info for interactive-debug tracing (optional) ─────────
    `ifdef DEBUG_TRACE
    a_handshake_trace:
    assert property (@(posedge pclk) disable iff (!presetn)
        $rose(penable))
        $info("[%0t] handshake started: paddr=0x%0h", $time, paddr);
    `endif
endmodule

What this code does. Five named assertions with severity matched to what each contract's violation means. $fatal is used only for the arbiter-state assertion — a corrupted state machine cannot produce meaningful subsequent results, so stopping the simulation is correct. $error is used for the three protocol-compliance assertions — real bugs that the regression should report and triage in one run. $warning is used for the latency-target assertion — the latency is within the spec maximum (no bug), but approaching the target deserves a flag. $info is used for handshake tracing under the DEBUG_TRACE conditional compile, so the trace runs in interactive debug only.

Expected output (fragment from a regression).

Simulation log fragment
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# *F,ASRTST: [142] a_arbiter_legal: arbiter_state=0xC not in valid set
#                  {IDLE, BUSY, COMPLETE, ERROR} — design has corrupted state
# Simulation stopped via $fatal.
#
# (Other assertions for this run were not evaluated after this point.)

The $fatal correctly stopped simulation — the arbiter state corruption means subsequent simulation cycles would produce arbitrary behaviour, not meaningful verification evidence. For comparison, if $error had been used:

Hypothetical $error path — multiple bugs surface in one run
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# *E,ASRTST: [142] a_arbiter_legal: arbiter_state=0xC ...
# *E,ASRTST: [148] a_paddr_range: paddr=0xDEADBEEF out of addressable range
# *E,ASRTST: [152] a_pready_within_16: PREADY > 16 cycles after ACCESS
# (Three real bugs surface; triage finds all of them in one regression.)

The trade-off matters: $fatal for genuinely impossible states (the design CAN'T continue meaningfully); $error for everything else (let the regression surface every bug in one run). Most production codebases use $error for ~95% of assertions and reserve $fatal for the 5% of contracts whose violation invalidates subsequent simulation.

Common Pitfalls

Pitfall 1 — $fatal everywhere

SystemVerilog — $fatal floods cause sequential debugging
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_bug_1:
assert property (...) else $fatal(1, "Bug 1");
a_bug_2:
assert property (...) else $fatal(1, "Bug 2");
a_bug_3:
assert property (...) else $fatal(1, "Bug 3");
 
// A regression with 3 real bugs:
//   Run 1: bug_1 fires, simulation stops. Engineer fixes bug 1.
//   Run 2: bug_2 fires (now visible since bug 1 is fixed). Fix bug 2.
//   Run 3: bug_3 fires. Fix bug 3.
//
// 3 sequential regression runs to find what 1 run with $error would have
// surfaced together.

Fix: use $error for protocol violations and design bugs. Reserve $fatal for impossible states only.

Pitfall 2 — $warning for real bugs

SystemVerilog — warning hides real bugs in CI
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_protocol_violation:
assert property (req |=> ack)
    else $warning("[%0t] req-ack violation", $time);
 
// In CI:
//   Build passes (warnings don't fail the build).
//   The bug ships.
//   Bring-up surfaces it.

Fix: real bugs use $error. CI gates on errors, not warnings. $warning is for legal-but-noteworthy conditions, not for protocol violations.

Pitfall 3 — Uninformative fail messages

SystemVerilog — message provides no actionable info
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_unhelpful:
assert property (...)
    else $error("Assertion failed");
 
// Log: "Assertion failed at fifo.sv:42"
// The engineer learns nothing — they already knew the assertion failed.

Fix: every fail message includes $time, the assertion name, and the offending signal values. Format like a bug report: where, when, with what values.

Pitfall 4 — Pass actions in production assertions

SystemVerilog — pass action floods the log
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_handshake:
assert property (req |=> ack)
    $info("[%0t] handshake completed", $time)     // fires every successful handshake
    else $error("[%0t] handshake violation", $time);
 
// In a regression that exercises the handshake 10,000 times:
//   10,000 $info messages flood the log.
//   The real $error fail (if any) is buried in the noise.

Fix: pass actions belong in interactive-debug-only contexts. Production regressions either don't use pass actions or wrap them in \ifdef DEBUG_TRACE` so they're off by default. Cover properties (Module 12.10) are the right tool for "the regression exercised this scenario" tracking.

Debug Lab — "The Regression Reports a Bug, But Engineers Can't Find It in the Waveform"

A subtle failure mode that surfaces when fail messages are uninformative: the regression correctly fires on a bug, but the verification engineer can't act on the report.

Buggy code
SystemVerilog — minimal fail messages
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_data_consistency:
assert property (@(posedge clk) disable iff (rst)
    write_valid |=> (read_data == $past(write_data)))
    else $error("Data mismatch");
 
a_addr_in_range:
assert property (@(posedge clk) disable iff (rst)
    addr inside {[BASE:TOP]})
    else $error("Address out of range");
Symptom

The regression runs. The dashboard reports:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
*E,ASRTST: Data mismatch
   Assertion at memctrl.sv:142 failed
*E,ASRTST: Address out of range
   Assertion at memctrl.sv:156 failed

The verification engineer reads the log. The message says "data mismatch" but doesn't say when it happened, what data was mismatched, what was expected, or what was actually read. The engineer has to open the waveform, find the exact cycle where the assertion fired (the simulator's log doesn't include the time), and reverse-engineer the offending values from the design's state. The same investigation, but for both failures separately. Hours of waveform-walking for what should have been a one-line read of the log.

Root cause

The fail messages don't include the information the engineer needs to act. "Data mismatch" is a category label, not a bug report. The format-string discipline from production verification environments is "include [%0t], include the offending values, include enough context that the verification engineer reads the log and can act without re-running."

The deeper failure: the engineer who wrote the assertion was the same engineer who would read the log. They knew implicitly which signals to inspect. When the verification engineer who triages the failure is different (or the same engineer six months later), the implicit knowledge is lost. The fail message must encode all the context the reader needs.

Fix

Format every fail message as a structured bug report: time, named assertion, offending values, expected values.

SystemVerilog — production-shape fail messages
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a_data_consistency:
assert property (@(posedge clk) disable iff (rst)
    write_valid |=> (read_data == $past(write_data)))
    else $error("[%0t] a_data_consistency: read_data=0x%0h expected=0x%0h "
                  "(write_valid=1 at $past, write_data=0x%0h)",
                  $time, read_data, $past(write_data), $past(write_data));
 
a_addr_in_range:
assert property (@(posedge clk) disable iff (rst)
    addr inside {[BASE:TOP]})
    else $error("[%0t] a_addr_in_range: addr=0x%0h out of range [0x%0h:0x%0h]",
                  $time, addr, BASE, TOP);

The discipline rule that prevents recurrence: every fail message includes [%0t] (the time), the assertion name, the actual values, and the expected values. Code review verifies the format-string discipline at the same scrutiny as the property's logic. PR review templates can include "all fail messages include [%0t] and offending signal values" as an explicit checklist item.

The complementary discipline: a regression-log post-processing script that flags assertions with fail messages shorter than ~50 characters as "underspecified" — a heuristic that catches the most common uninformative-message bugs. The script generates a verification-engineer-actionable report from the structured fail messages; assertions that don't follow the format get flagged for cleanup.

Industry Context — Where Severity Discipline Lives

  • AMBA protocol VIPs. Every shipping AMBA VIP uses $error for protocol-compliance assertions, $warning for performance-margin assertions (latency approaching spec maximum), $fatal for impossible-state assertions (corrupted internal state machines). The severity choices are documented in the VIP's assertion-style guide.
  • UVM frameworks. UVM's uvm_error, uvm_warning, uvm_info, uvm_fatal map to the SVA $error, $warning, $info, $fatal system tasks. The UVM library uses the same severity discipline — uvm_error for protocol violations, uvm_fatal for environment-corruption conditions.
  • DDR / LPDDR controllers. JEDEC-timing assertions use $error for spec violations (tRCD violated, tREFI exceeded). Performance-corner assertions use $warning (timing margin near zero but not violated). Internal state machines use $fatal (controller in undefined state).
  • PCIe / USB / Ethernet protocols. Protocol-compliance assertions use $error. Coverage-target tracking uses $info under conditional compile. Severity levels are part of the spec-traceability matrix — each assertion's severity is documented against the spec section's criticality.
  • Safety-critical / DO-254 / ISO 26262 flows. Auditors verify severity choices match spec criticality. A $warning for a safety-critical contract violation is a certification finding; the severity must match the spec's classification of the rule (mandatory vs recommended).
  • CI / regression dashboards. Production CI flows gate the build on $fatal and $error counts; $warning counts are tracked but don't fail the build. The severity discipline is what makes the dashboard's pass/fail signal trustworthy — a severity mismatch (using $warning for a real bug) means the dashboard reports green when the regression actually has bugs.

The pattern across these uses: severity is a spec-traceability decision, not a stylistic preference. Each severity level corresponds to a specific spec criticality; the choice is documented as part of the assertion's contract.

Interview Q&A — Ten Questions You Will Be Asked

$fatal(N, msg) — stops simulation immediately; for impossible states. $error(msg) — continues simulation, counts the error; the canonical fail action for protocol violations and design bugs. $warning(msg) — continues simulation, counts the warning; for legal-but-suspicious conditions. $info(msg) — continues simulation, no counter; for debug tracing. The severity choice matches what the violation actually means for the design, not how alarming the message sounds.

Coding Guidelines — Seven Rules for Severity Discipline

  1. Match severity to what the violation actually means for the design. $fatal for impossible states; $error for protocol violations and design bugs; $warning for legal-but-noteworthy; $info for interactive debug tracing.
  2. Use $error as the default fail action. Reserve $fatal for the 5% of contracts whose violation invalidates subsequent simulation; reserve $warning for non-bug conditions; reserve $info for diagnostic tracing.
  3. Format every fail message as a structured bug report. Include [%0t], the assertion name, the offending values, and the expected values. The message must answer where, when, and with what values without re-running.
  4. Never use $warning for real bugs. CI build gates check errors, not warnings; using $warning for protocol violations makes the bug invisible to the CI dashboard.
  5. Avoid pass actions in production assertions. Pass actions fire on every successful evaluation, flooding the log. Use \ifdef DEBUG_TRACE` to enable interactive-debug pass actions; rely on cover properties for regression-time scenario tracking.
  6. Code review verifies severity-to-spec-criticality mapping. Each assertion's severity is checked against the spec section it implements. A $warning for a spec-mandatory rule is rejected at review.
  7. CI flow gates reflect the severity discipline. Build pass = 0 errors AND 0 fatals (warnings are tracked but don't gate). For spec-critical assertions specifically, gates can also check that warnings are zero.

Summary

The four SVA severity tasks — $fatal, $error, $warning, $info — control what happens when an assertion fires. $fatal stops simulation; $error continues and counts; $warning continues with a warning flag; $info traces without affecting counts. Match severity to what the violation actually means: $error for the 95% of protocol violations and design bugs, $fatal for the 5% of impossible states that invalidate subsequent simulation, $warning for legal-but-noteworthy conditions, $info for interactive-debug tracing. Format every fail message as a structured bug report — [%0t], the assertion name, the offending values, the expected values — so the verification engineer can act without re-running. The format-string discipline is what turns a regression log from "247 messages" into "actionable verification evidence." Module 12.12 closes Module 12 with the assertion-control system tasks ($assertoff, $asserton, $assertkill) — the runtime controls for enabling and disabling assertions during specific simulation phases.