Skip to content

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:

  1. Add message template to SAGE_ERRORS in error_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"
    }
}
  1. Optionally create a custom exception:
class NewError(Exception):
    def __init__(self, context: str):
        self.context = context
  1. Add handling in the appropriate try/except block.

Monitoring

All errors are logged with full context:

logger.error(f"Error in message handler: {str(e)}", exc_info=True)

This preserves: - Full stack trace - Error type and message - Request context

Friendly user messages are separate from detailed logs for debugging.