Error Style Guide
Last updated
Write actionable errors — wording, streams, exit codes, diagnostics, glyphs, and live-render rules.
On this page
How comic-dl talks to its user when things go wrong. A user should always know what went wrong, why, and what to do next — without ever seeing a traceback or an internal module path.
The three hard rules
- Start lowercase, end with a period. Messages read mid-sentence.
- Lead with the object/context, then a colon, then the problem.
- Every error carries an actionable hint where one exists.
And the two “never”s: never show implementation details in level-0 output (tracebacks, exception class names, module paths, internal state); never drop user-actionable details (“Permission denied”, the offending value, the host, the invalid switch).
Canonical examples
invalid size: '10x' (e.g. 100MB, 2GB, 512KB, 104857600)
Invalid chapter selection '5-2': reversed range 5-2.
File not found: /nonexistent/urls.txt
Could not create output directory: /data/dl (Permission denied)
Download a series first, or point -o at the right output root.
The last shows the two-line form: print_error_detail(context, reason, hint).
Streams
| Stream | Contains |
|---|---|
| stdout | Final results: “Saved” lines, summaries, tables, JSON, --help, self version |
| stderr | Everything else: banners, progress, warnings, errors, retries, prompts |
A stray print() to stdout in a flow that ends in JSON (machine output) breaks
consumers.
Message helpers
Every message goes through a ui.py helper. Don’t compose Rich Console
calls ad hoc for user-facing text.
| Helper | Stream | Use for |
|---|---|---|
print_error(message) |
stderr | One-line failure |
print_warning(message) |
stderr | Non-fatal caution |
print_error_detail(context, reason, hint) |
stderr | Failure with reason and hint |
report_error(exc, context, hint) |
stderr | Map exception to message + exit code |
print_success(message) |
stdout | Successful result |
print_dim(message) |
stderr | Muted context |
report_error is the one door that sees a raw exception — it maps it to a
message and an exit code, and prints the traceback only at -vvv.
Exit codes
| Code | Constant | Use when |
|---|---|---|
| 0 | EXIT_OK |
Success |
| 1 | EXIT_ERROR |
Download/library/network failure |
| 2 | EXIT_USAGE |
Bad flag, bad value, unknown command |
| 130 | EXIT_INTERRUPTED |
Ctrl-C / SIGTERM |
Use the named constants — never bare integers. See Exit Codes for categories and interrupt behavior.
Mapping errors to exit codes
errors.py defines the taxonomy; each type carries its exit code. New types
subclass existing ones — never a bare ComicError with a code stashed in a
message string. The mapping rule: usage errors → 2, operational failures →
1. A wrong flag must not look like a download failure.
Two machine-readable hooks ride on top of the human message and never appear
in it: kind (usage, download, scrape, timeout, library, …) lets
JSON mode and the library branch without parsing prose, and
ScrapeError.site_error_code carries the SITE_* constants for site-support
checks.
Verbosity and diagnostics
| Level | Flag | Shows |
|---|---|---|
| 0 | (default) | Progress, status, results, warnings, errors, summary |
| 1 | -v |
Source, output paths, options, metadata |
| 2 | -vv |
HTTP requests, retries, timing, size estimates |
| 3 | -vvv |
Response headers, workflow trace, tracebacks |
Rules:
- Level-0 UI is only the
print_*helpers, nevervlog(). vlog(level, message, tag=...)writes to stderr only at the right level. Tags are a fixed vocabulary:[http],[retry],[scrape],[timing],[download],[archive].- Tracebacks print only at
-vvv, viareport_error.
Bug reports attach -vvv output (see Reporting); the levels
make that debuggable without exposing internals at level 0.
Glyphs and ASCII fallback
All status symbols come from the single ui.glyphs() set. When the terminal
cannot emit UTF-8 — or COMIC_DL_ASCII=1 is set — the UI falls back to
ASCII, and the non-TTY spinner is always ASCII (no mojibake in piped output).
Never hardcode ✓/✗/spinner frames in a string; route them through
glyphs(). Tests assert with output.replace("\n", "") precisely because
glyph runs are width- and encoding-dependent.
Live rendering: the two jitter rules
- Fixed-width numeric slots — every ticking readout (bytes, speed, ETA) is right-aligned to a constant column width.
- One frame owner per
Live— eachLivehas exactly one render loop; row/state events mark it dirty and the loop rebuilds at most once per tick.
A jittering frame reads like a crash under a progress bar. When you change a live renderer, re-read these and re-test with a slow terminal.