Handling USSD Errors
USSD has three recurring failure modes: invalid input, session timeouts, and backend errors. Handling each cleanly keeps users from abandoning mid-flow. This guide shows the patterns.
⚠️ The Three Failure Modes
| Failure | Cause | Response |
|---|---|---|
| Invalid input | Wrong key / out-of-range | Re-prompt with valid options |
| Timeout | Session expired by carrier | Graceful END, suggest re-dial |
| Backend error | Your service or upstream failed | END with a clear apology |
🔁 Invalid Input
Re-prompt instead of dead-ending. Cap retries so users aren't trapped.
def handle_menu(text, attempt):
if text in ('1', '2', '3'):
return route(text)
if attempt >= 2:
return "END Too many invalid entries. Please dial again."
return "CON Invalid choice.\n1. Balance\n2. Airtime\n3. Send money"
function validate(input, valid) {
if (valid.includes(input)) return { ok: true };
return { ok: false, msg: `CON Invalid entry. Choose: ${valid.join(', ')}` };
}
⏱️ Timeouts
You can't extend a carrier session, but you can design around it and recover on re-dial.
- Keep each step completable in a few seconds.
- Persist progress keyed by phone so a re-dial can offer "Resume".
- Don't leave half-finished transactions in a pending state — roll them back.
def on_resume(phone):
pending = store.get(f"resume:{phone}")
if pending:
return f"CON Resume {pending['action']}?\n1. Yes\n2. Start over"
return root()
🛠️ Backend Errors
Never expose stack traces or hang. Fail fast with a friendly message and a fallback.
async function handle(req) {
try {
return await process(req);
} catch (err) {
log(err);
return 'END Sorry, something went wrong. Please try again shortly.';
}
}
For money operations, confirm the final state out-of-band by SMS so the user has a record even if the session dropped.