Error Recovery UX: Sage-Voice Error Messages¶
Last Updated: January 9, 2026
Overview¶
When errors occur, Sage responds with friendly, casual error messages that stay in character while providing clear recovery paths. No technical jargon, no robotic responses.
Key principle: Errors should feel like Sage having a temporary hiccup, not a system failure.
Error Message Categories¶
OAuth Errors¶
| Error Type | Service | Message |
|---|---|---|
| Token expired | Calendar | "hmm my calendar access expired - can u reconnect? {link}" |
| Token expired | Gmail | "looks like i lost access to ur email - wanna reconnect? {link}" |
| Not connected | Calendar | "i don't have access to ur calendar yet - want me to connect? {link}" |
| Not connected | Gmail | "i can't see ur email rn - wanna connect it? {link}" |
API/Service Errors¶
| Error Type | Message |
|---|---|
| Timeout | "ugh sorry something's being slow rn, can u try again in a sec?" |
| Generic API error | "hmm something went wrong on my end - try again?" |
| Rate limited | "i'm getting a lot of requests rn, give me a sec and try again" |
Workflow Errors¶
| Error Type | Message |
|---|---|
| Workflow failed | "hmm that didn't work, can u try again?" |
| Workflow timeout | "that's taking longer than expected - wanna try again?" |
MiniApp Errors¶
| Error Type | Message |
|---|---|
| General error | "sorry i'm having trouble with that rn - wanna try again in a sec?" |
| Session expired | "looks like we lost our place - wanna start over?" |
Other Errors¶
| Error Type | Message |
|---|---|
| Memory failed | Silent (don't interrupt conversation) |
| Memory search failed | "hmm i'm having trouble remembering things rn, but i can still chat" |
| Reminder failed | "hmm something went wrong setting that reminder - wanna try again?" |
| Timezone needed | "i need to know ur timezone to set reminders - what timezone are u in?" |
| Classification failed | "sorry i didn't quite get that - can u say it differently?" |
| Boundary error | "got it, but i'm having trouble with that right now. try again in a sec?" |
| Unknown error | "sorry i'm having a moment, can you say that again?" |
Error Message Principles¶
DO¶
- Use lowercase (Sage's voice)
- Be casual and friendly
- Offer a recovery path
- Keep it short
- Use contractions (can't, don't, wanna)
DON'T¶
- Use technical terms (API, timeout, exception)
- Include error codes
- Sound robotic or formal
- Make users feel at fault
- Leave users without next steps
Exception Classes¶
Custom exceptions for better error handling:
class OAuthExpiredError(Exception):
"""Raised when OAuth token has expired and refresh failed."""
def __init__(self, service: str):
self.service = service # "calendar" or "gmail"
class OAuthNotConnectedError(Exception):
"""Raised when user hasn't connected the required OAuth service."""
def __init__(self, service: str):
self.service = service
class WorkflowError(Exception):
"""Raised when a workflow execution fails."""
def __init__(self, workflow_id: str):
self.workflow_id = workflow_id
class MiniAppError(Exception):
"""Raised when a MiniApp operation fails."""
def __init__(self, miniapp_id: str):
self.miniapp_id = miniapp_id
class RateLimitError(Exception):
"""Raised when rate limit is exceeded."""
pass
Error Handling Flow¶
Main Message Handler (message_handler.py)¶
try:
return await self._process_message(...)
except OAuthExpiredError as e:
auth_link = generate_oauth_link(e.service, user_phone)
error_msg = get_error_response("oauth_expired", service=e.service, context={"auth_link": auth_link})
await send_func(error_msg)
return False
except OAuthNotConnectedError as e:
auth_link = generate_oauth_link(e.service, user_phone)
error_msg = get_error_response("oauth_not_connected", service=e.service, context={"auth_link": auth_link})
await send_func(error_msg)
return False
except WorkflowError as e:
await send_func(get_error_response("workflow_failed"))
return False
except MiniAppError as e:
await send_func(get_error_response("miniapp_error"))
return False
except RateLimitError as e:
await send_func(get_error_response("rate_limited"))
return False
except Exception as e:
logger.error(f"Error in message handler: {str(e)}", exc_info=True)
await send_func(get_error_response("unknown"))
return False
Error Message API¶
get_error_response()¶
def get_error_response(
error_type: str, # Key into SAGE_ERRORS dict
service: Optional[str] = None, # For OAuth errors
context: Optional[dict] = None # Template variables
) -> Optional[str]:
Usage examples:
# Simple error
get_error_response("workflow_failed")
# → "hmm that didn't work, can u try again?"
# OAuth error with link
get_error_response("oauth_expired", service="calendar", context={"auth_link": "https://..."})
# → "hmm my calendar access expired - can u reconnect? https://..."
# Silent error (returns None)
get_error_response("memory_failed")
# → None (don't send anything)
Silent Errors¶
Some errors should not interrupt the conversation:
| Error Type | Why Silent |
|---|---|
memory_failed |
Memory is supplementary - conversation can continue |
| Non-critical background tasks | User shouldn't see every background hiccup |
For silent errors, get_error_response() returns None.
File Locations¶
| Component | Location |
|---|---|
| Error messages | app/orchestrator/error_messages.py |
| Exception classes | app/orchestrator/error_messages.py |
| Error handling | app/orchestrator/message_handler.py (lines 453-510) |
Example Error Flows¶
OAuth Token Expired¶
User: "am I busy tomorrow?"
[Calendar API returns 401 - token expired]
Sage: "hmm my calendar access expired - can u reconnect? https://archety.com/connect/calendar"
Rate Limit Hit¶
User: "tell me about X"
[OpenAI returns 429]
Sage: "i'm getting a lot of requests rn, give me a sec and try again"
Unknown Error¶
User: "do something complex"
[Unexpected exception in processing]
Sage: "sorry i'm having a moment, can you say that again?"
[Error logged with full stack trace for debugging]
Adding New Error Types¶
To add a new error type:
- Add message template to
SAGE_ERRORSinerror_messages.py:
SAGE_ERRORS = {
# ...existing errors...
"new_error_type": "casual error message here - try again?",
# For service-specific errors:
"service_error": {
"service_a": "message for service a",
"service_b": "message for service b",
"default": "fallback message"
}
}
- Optionally create a custom exception:
- Add handling in the appropriate try/except block.
Monitoring¶
All errors are logged with full context:
This preserves: - Full stack trace - Error type and message - Request context
Friendly user messages are separate from detailed logs for debugging.