Loading search…

Explain with AI
Edit this page

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

  1. Start lowercase, end with a period. Messages read mid-sentence.
  2. Lead with the object/context, then a colon, then the problem.
  3. 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, never vlog().
  • 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, via report_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

  1. Fixed-width numeric slots — every ticking readout (bytes, speed, ETA) is right-aligned to a constant column width.
  2. One frame owner per Live — each Live has 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.