By Sagar Shankaran, Founder of CallSphere
Design error messages for AI agents that categorize failures, provide helpful recovery paths, maintain user trust during outages, and turn mistakes into positive experiences.
Key takeaways
Every AI agent will fail. APIs go down, models hallucinate, users submit invalid input, and rate limits get hit. The difference between an agent users trust and one they abandon is not the frequency of errors — it is how the agent communicates and recovers from them.
Generic error messages like "Something went wrong" are the conversational equivalent of a brick wall. They tell the user nothing about what happened, why, or what to do next. Thoughtful error design turns failure moments into demonstrations of reliability.
Not all errors are equal. Categorize them by cause and user-facing impact to deliver appropriate responses:
flowchart LR
INPUT(["User intent"])
PARSE["Parse plus<br/>classify"]
PLAN["Plan and tool<br/>selection"]
AGENT["Agent loop<br/>LLM plus tools"]
GUARD{"Guardrails<br/>and policy"}
EXEC["Execute and<br/>verify result"]
OBS[("Trace and metrics")]
OUT(["Outcome plus<br/>next action"])
INPUT --> PARSE --> PLAN --> AGENT --> GUARD
GUARD -->|Pass| EXEC --> OUT
GUARD -->|Fail| AGENT
AGENT --> OBS
style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
style GUARD fill:#f59e0b,stroke:#d97706,color:#1f2937
style OBS fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style OUT fill:#059669,stroke:#047857,color:#fff
from enum import Enum
from dataclasses import dataclass
class ErrorCategory(Enum):
INPUT_VALIDATION = "input_validation"
KNOWLEDGE_GAP = "knowledge_gap"
EXTERNAL_SERVICE = "external_service"
RATE_LIMIT = "rate_limit"
AMBIGUOUS_REQUEST = "ambiguous_request"
PERMISSION_DENIED = "permission_denied"
MODEL_ERROR = "model_error"
TIMEOUT = "timeout"
@dataclass
class AgentError:
category: ErrorCategory
internal_message: str # For logs — may contain sensitive details
user_message: str # Shown to user — never exposes internals
recovery_suggestions: list[str]
can_retry: bool
escalate_to_human: bool
ERROR_TEMPLATES: dict[ErrorCategory, dict] = {
ErrorCategory.INPUT_VALIDATION: {
"user_message": "I couldn't process that input. {specific_issue}.",
"recovery_suggestions": [
"Try rephrasing your request",
"Check the format — {expected_format}",
],
"can_retry": True,
"escalate_to_human": False,
},
ErrorCategory.KNOWLEDGE_GAP: {
"user_message": (
"I don't have information about {topic} in my knowledge base."
),
"recovery_suggestions": [
"Try asking about a related topic",
"I can connect you to a specialist who might know",
],
"can_retry": False,
"escalate_to_human": True,
},
ErrorCategory.EXTERNAL_SERVICE: {
"user_message": (
"I'm having trouble reaching {service_name} right now."
),
"recovery_suggestions": [
"I'll automatically retry in a moment",
"You can also try again in a few minutes",
],
"can_retry": True,
"escalate_to_human": False,
},
ErrorCategory.RATE_LIMIT: {
"user_message": (
"I've hit a temporary limit on requests. This usually "
"resolves within {wait_time}."
),
"recovery_suggestions": [
"Wait a moment and try again",
"If urgent, I can transfer you to a human agent",
],
"can_retry": True,
"escalate_to_human": True,
},
}
Follow the What-Why-Next pattern for every error message:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
def build_error_message(error: AgentError) -> str:
"""Build a user-friendly error message following What-Why-Next pattern."""
parts = []
# WHAT happened
parts.append(error.user_message)
# WHY (when appropriate and non-technical)
if error.category == ErrorCategory.EXTERNAL_SERVICE:
parts.append(
"This is a temporary issue on our end, not anything you did wrong."
)
elif error.category == ErrorCategory.INPUT_VALIDATION:
parts.append(
"I need the information in a specific format to look it up."
)
# NEXT — what the user can do
if error.recovery_suggestions:
parts.append("Here's what you can try:")
for suggestion in error.recovery_suggestions:
parts.append(f" - {suggestion}")
if error.escalate_to_human:
parts.append(
"Or I can connect you to a human agent who can help directly."
)
return "\n".join(parts)
A concrete example of the output: "I'm having trouble reaching our shipping system right now. This is a temporary issue on our end, not anything you did wrong. Here's what you can try: I'll automatically retry in a moment. You can also try again in a few minutes."
When retrying automatically, keep the user informed rather than leaving them in silence:
import asyncio
class RetryWithFeedback:
"""Retry an operation while communicating progress to the user."""
def __init__(self, max_retries: int = 3, base_delay: float = 2.0):
self.max_retries = max_retries
self.base_delay = base_delay
async def execute(self, operation, send_message) -> dict:
for attempt in range(1, self.max_retries + 1):
try:
result = await operation()
if attempt > 1:
await send_message("Got it! Here's what I found:")
return {"success": True, "data": result}
except Exception as e:
if attempt < self.max_retries:
wait_time = self.base_delay * (2 ** (attempt - 1))
await send_message(
f"Still working on it... retrying "
f"(attempt {attempt + 1} of {self.max_retries})"
)
await asyncio.sleep(wait_time)
else:
return {
"success": False,
"error": str(e),
"message": (
"I wasn't able to complete that after several "
"attempts. Let me connect you with someone "
"who can help directly."
),
}
When a subsystem fails, offer partial functionality rather than complete failure:
class GracefulDegradation:
"""Provide degraded but useful responses when services are down."""
def __init__(self, service_status: dict[str, bool]):
self.services = service_status
def get_order_info(self, order_id: str) -> str:
if self.services["order_api"]:
return self._fetch_full_order(order_id)
if self.services["cache"]:
cached = self._get_cached_order(order_id)
return (
f"Our order system is being updated right now, but "
f"here's the last status I have from {cached['timestamp']}: "
f"{cached['summary']}. For the very latest status, "
f"check your email for tracking updates."
)
return (
f"Our order system is temporarily unavailable. "
f"You can check your order status at acme.com/orders "
f"or reply with 'human' to speak with an agent."
)
def _fetch_full_order(self, order_id: str) -> str:
return ""
def _get_cached_order(self, order_id: str) -> dict:
return {}
Each degradation level still provides value. The user always has a path forward.
Every user-facing error is a data point for improvement. Structure your error logs for analysis:
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
import json
from datetime import datetime
def log_agent_error(
error: AgentError,
user_input: str,
conversation_id: str,
session_context: dict,
) -> None:
"""Log structured error data for analysis and improvement."""
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"conversation_id": conversation_id,
"error_category": error.category.value,
"internal_message": error.internal_message,
"user_input_length": len(user_input),
"user_input_hash": hash(user_input), # Privacy-safe
"recovery_offered": error.recovery_suggestions,
"escalated": error.escalate_to_human,
"retryable": error.can_retry,
"session_turn_count": session_context.get("turn_count", 0),
}
# Ship to your analytics pipeline
print(json.dumps(log_entry))
Notice the log captures the error context and recovery action without storing raw user input, preserving privacy while maintaining debuggability.
Keep error messages in the same conversational tone as normal responses. Avoid switching to a formal or robotic register when errors occur. If your agent normally uses contractions and friendly language, the error message should too. The user should feel like the same "person" is still talking, just honestly explaining a hiccup.
Never show stack traces, error codes, or internal service names to end users. These details are meaningless to most users and can be a security risk. Instead, log technical details server-side and show the user a plain-language explanation. The one exception is providing a reference ID ("Error ref: ABC123") so support staff can look up the technical details if the user escalates.
Three retries with exponential backoff is a good default. After the first failure, wait 2 seconds. After the second, wait 4 seconds. After the third failure, stop retrying and offer alternatives — human escalation, a different approach, or a callback. Total elapsed time should never exceed 30 seconds of user-visible waiting.
#ErrorHandling #UX #AIAgents #ConversationDesign #Recovery #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to the personal AI assistant market: best AI assistant apps, business-grade options, and how CallSphere's voice agent fits in.
A founder's guide to free AI agents, low-code AI agent builders, and how to know when you should pay for a real platform like CallSphere.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
Chatbot app vs ChatGPT in 2026: a founder's clear take on the difference, when to use which, and how a real AI chatbot app development works.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Head-to-head: OpenAI Frontier and Anthropic's managed agent stack — strengths, fit, and what each means for enterprise AI voice and chat deployment.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.