Handle TOVIO errors in a script
The structured error shape, and how to branch on it correctly.
Written By Dustin
Last updated About 3 hours ago
Every failure under --json produces one object on stdout under a top-level error key. Here is a real one:
{ "error": { "code": "TVO-CLI-003", "area": "cli", "category": "usage", "exit_class": "usage", "retryability": "after-user-action", "title": "Not a TOVIO repository", "cause": "There is no .tovio directory here, so this command has nothing to act on.", "remediation": [ { "text": "Initialize a repository here", "command": "tovio init" } ], "context": {}, "exit_code": 2, "docs": "https://tovio.dev/errors/TVO-CLI-003" }}What to branch on
codeis the primary key. It is stable across versions and it is what you match on when you care about one specific failure.retryabilitytells you whether to try again. Only the explicitly transient class is worth a retry loop.exit_classis the coarse category — use it when you want to treat every permission failure alike without enumerating codes.
The mistake to avoid
Do not infer retryability from the area prefix or the numeric exit. It is tempting — surely everything under sync is worth retrying — and the contract rules it out, because the mapping is not one-to-one. A network blip and a refused wire-protocol version can share an area and an exit code, and retrying the second one just produces it again. The field exists so you do not have to guess.
Nor should you hard-code the exit numbers. There are more classes than the common ones, including a case where the operation succeeded and a post-event check failed afterwards — a script that maps unknown exits to "the command failed" will report a commit that in fact committed. Read exit_class.
The parts that are safe to show a human
title is the one-line what. cause is the why, stated as required-versus-actual. remediation is an ordered list, most likely first, each entry with text and often a runnable command. Printing the first remediation command is usually the most useful thing a script can do with a failure.
Every code with a documentation page also carries a docs link, of the form tovio.dev/errors/<code>. The catalog behind those pages is the system of record for what each code means; this article is only about consuming the envelope.
What context carries
context holds code-specific structured facts with stable keys per code — the path, the policy expression, your attributes, what was missing. It never carries anything you are not authorized to read: it is your facts plus the public policy expression, never the plaintext and never another principal's attributes. That is a guarantee you can rely on when deciding whether an error is safe to log.
Read defensively
Tolerate fields you do not recognise, and tolerate the absence of the optional ones — docs in particular is absent for the generic fallback code. New fields are additive; a reader that fails on an unfamiliar key will break on an upgrade that broke nothing.
Quiet mode
--quiet never suppresses errors. It prints the what line and the first remediation command to stderr, and the exit code is unchanged. It is the right mode for a script that wants silence on success and a usable line on failure without parsing JSON.
A conflict is not an error
Creating a conflict exits 0. tovio conflicts exits 0 whether or not there are any. A conflict in TOVIO is data — a first-class object you resolve — not a failed operation, and a script that treats a non-empty conflict list as a failure is encoding the wrong model.