In the previous step you learned to not throw exceptions. you return { ok: false, error: ... } instead of crashing. That was the first 50%. the second 50% is what you put inside error.
There's a huge difference between these two errors:
// Useless
{ ok: false, error: "didn't work" }
// Actionable
{
ok: false,
error: {
code: "capacity_too_small",
message: "Room room-B-04 has capacity 4, but you requested 8 people.",
available_capacity: 4,
requested_capacity: 8,
recovery_hint: "Try list_rooms({ min_capacity: 8 }) to find a bigger room."
}
}With the second, the agent can:
list_rooms with min_capacity: 8).With the first, the agent knows nothing. most likely it retries, or tells the user "didn't work" and cuts the conversation.
{
ok: false,
error: {
code: string, // machine-readable, snake_case
message: string, // human-readable
...extra_context // data to reason with
}
}The extra fields are what distinguish a useful error from a useless one. Every code should carry case-specific info:
not_found → what you were looking for (looked_up: "room-B-04").permission_denied → which permission is missing (required_role: "admin").validation_failed → which field and which rule (field: "amount", rule: "min:1").conflict → what it conflicts with (conflicts_with: {...}).rate_limited → when retry is allowed (retry_after: "30s").recovery_hint fieldA technique that pays off: add recovery_hint as a free string, suggesting the next step. The agent reads that sentence and almost always follows it. It's like giving it advice from your side.
recovery_hint: "Try list_available_rooms({ min_capacity: 8, deck: 'B' }) to find a fitting room."On the right you have book_room. The starter works on the happy path. Your job: cover 4 error cases with rich messages, each with its specific code and extra data so the agent can act.
The agent is only as smart as the information you give it. Poor errors = poor agent. Rich errors = agent that self-corrects.