Enhancing Code with LLMs: Assertion Messages for Debugging
As developers lean on large language models to accelerate coding, a quiet but powerful frontier appears in the space between tests and runtime checks: assertion messages. These messages don’t just say “something failed.” They explain what happened, why it happened, and how to fix it. When crafted well, assertion messages become living documentation for your code’s invariants, edge cases, and intended behavior—turned into actionable guidance rather than cryptic hints.
Why assertion messages matter in the era of AI-assisted development
LLMs can generate context-rich messages that reveal the chain of reasoning a failure followed. They can summarize inputs, preconditions, and runtime state in a concise, readable format. That clarity is especially valuable in complex systems where a simple “assertion failed” leaves you staring at a stack trace and guessing which invariant was violated. By guiding engineers to the root cause faster, well-designed assertion messages reduce debugging time, improve incident response, and help onboard new teammates who encounter unfamiliar code paths.
What makes a great assertion message?
A high-quality assertion message should be clear, actionable, and reproducible. Consider these elements:
- Context: what condition was expected vs. what was observed.
- Inputs: representative values that led to the failure, without leaking sensitive data.
- State snapshot: relevant environment or configuration that influenced the result.
- Invariants: the underlying principle that must hold true for correctness.
- Guidance: concrete next steps or a suggested fix to help triage.
- Determinism: the message should be stable across runs when inputs are the same.
When these bits are present, an assertion message becomes a small, dependable debugging companion that travels with the codebase.
How LLMs can improve assertion messages
LLMs excel at transforming raw failure data into structured, readable narratives. They can:
- Summarize why a condition failed, not just that it did.
- Embed structured data alongside the human-readable text, such as JSON blocks with inputs and environment details.
- Suggest actionable next steps tailored to the failure type (validation, invariants, or boundary conditions).
- Keep messages consistent across a codebase when using a shared prompting template.
The key is to balance automation with guardrails: you want messages that are informative yet safe, avoiding secrets leakage and preserving performance in hot code paths.
Patterns for integrating LLM-generated messages in tests and runtime checks
- Template-driven prompts: design a small set of templates that the LLM fills with current inputs and state. This ensures consistency and reduces drift across modules.
- Structured message payloads: pair human-readable text with a machine-parsable block (for example, a JSON object) that captures actual, expected, inputs, and environment.
- Runtime vs. test-time differentiation: use lightweight, deterministic messages in production paths and richer, debug-oriented messages in test environments.
- Security and privacy guards: scrub sensitive data before including inputs in messages; consider redaction rules and access controls for logs.
- Caching and determinism: cache LLM outputs for identical failure scenarios to avoid flakiness and reduce latency.
“Good debugging messages do more than describe a failure; they guide you to the invariant that prevented it.”
A practical example
Here’s a compact illustration in Python of how an LLM-generated message might be used in a runtime assertion. The example keeps the message human-friendly while embedding a structured payload for tooling to consume.
def llm_build_message(template, payload):
# Placeholder for an LLM call that returns a plain-text message
# plus an embedded JSON payload with failure context.
# For this example, we'll simulate the output.
human = (
f"Assertion failed: {payload['invariant']}. "
f"Observed {payload['actual']} with inputs {payload['inputs']}."
)
structured = {
"assertion": payload['invariant'],
"actual": payload['actual'],
"inputs": payload['inputs'],
"environment": payload['environment'],
"suggestion": "Review preprocessing and invariants for non-negative values."
}
return human, structured
def assert_with_llm_condition(condition, invariant, inputs, environment):
if not condition:
human, structured = llm_build_message("assertion_failure", {
"invariant": invariant,
"actual": inputs.get("value"),
"inputs": inputs,
"environment": environment
})
# You can log both forms and raise with the human-friendly version.
log_debug(human)
raise AssertionError(human) # or incorporate structured into logs
# tooling could also attach the structured blob to logs/telemetry
In real setups, replace the placeholder with a real LLM call and a robust redaction policy. The pattern remains: present a readable message for humans, plus a structured payload for programmatic processing and metrics.
Best practices and guardrails
: start with critical invariants and narrow the surface area you instrument with LLM-generated messages. : track debugging time, incident rate, and user impact to justify the added complexity. : redact or omit credentials and tokens; consider environment-only narratives for production. : adopt shared message templates and a versioned schema so teams can reason about changes over time. : keep messages concise for hot paths; reserve richer, verbose output for logs and diagnostics.
Choosing the right level of user-facing detail
Not every failure benefits from the same flavor of message. Some failures are developer-facing and should emphasize actionable debugging hints, while others affect end-user workflows and require clearer guidance on remediation steps. The best approach treats assertion messages as two-tier: a succinct, safe primary message and an optional, richer secondary narrative available to developers or in diagnostic logs.
Starting small—define a minimal, structured format and a single, well-tested prompt for one or two invariant types. As your team grows comfortable, expand to broader coverage, more invariants, and improved machine-generated guidance. The result is code that speaks clearly about its own expectations, with LLMs acting as a thoughtful co-pilot for debugging rather than a black box risk.