Skip to content

Errors & exit codes

wordlive translates pywin32's pywintypes.com_error into a small, typed exception hierarchy. The CLI maps those exceptions to deterministic exit codes so LLM tool-use loops can branch on the failure mode without parsing error text.

Exception hierarchy

Exception
└── WordliveError
    ├── WordNotRunningError
    ├── DocumentNotFoundError
    ├── AnchorNotFoundError
    │   └── StyleNotFoundError
    ├── AmbiguousMatchError
    ├── ReplaceVerificationError
    ├── ImageSourceError
    ├── SnapshotError
    ├── WordBusyError
    └── ComError

WordliveError is the catch-all base — except wordlive.WordliveError catches every typed error wordlive raises. Anything that wasn't a COM error in the first place (e.g. a ValueError from your own code) bubbles up unchanged.

Reference

WordliveError

Base class. Catch this if you want one try for every wordlive failure.

WordNotRunningError

No Word instance is running. Raised by attach() and by connect(launch_if_missing=False). Not retryable within a session — Word has to actually be running.

DocumentNotFoundError

The requested document isn't open. Raised by word.documents[name] and by word.documents.active when no document is active. The missing name is on the exception's .name attribute.

AnchorNotFoundError

A bookmark, content control, heading, paragraph, table cell, comment, range, list, section, or header/footer you asked for doesn't exist — or a find/replace --find pattern matched zero occurrences (in that case .kind == "find" and .name is the search string). .kind names the thing that was missing ("bookmark", "heading", "paragraph", "table cell", "comment", "range", "list", "section", "header", "footer", …) and .name is what you asked for. Retryable after refreshing the outline / bookmark list or reading the current content — the document may have changed since you last looked.

StyleNotFoundError

A paragraph or character style you asked for isn't defined in the document. Subclass of AnchorNotFoundError — it shares the same exit code (2) and the same retry guidance, and except AnchorNotFoundError catches it too. .kind is always "style" and .name is the requested style name. Raised by Document.styles[name], Anchor.apply_style(name), and Anchor.insert_paragraph_before/after(text, style=name). Retryable after reading doc.styles.list() to see what's actually defined. To MCP clients it surfaces a distinct code: "style_not_found" (the CLI exit code is still 2), so a missing style is told apart from a missing bookmark/heading.

AmbiguousMatchError

A fuzzy find_replace matched more than one occurrence and the caller didn't say all=True or pass an occurrence. The exception carries .find (the search string) and .matches (a list of {anchor_id, start, end, text} dicts) so an agent can pick a specific occurrence and retry. Retryable by narrowing the call with occurrence=N or all=True.

ReplaceVerificationError

A fuzzy find_replace resolved a write target whose text didn't match what was located, so wordlive refused to write rather than corrupt the document. This guards the case where Range.Text offsets diverge from Word's document positions across table structure — a whole-document replace could otherwise overwrite a neighbouring cell while returning success. Carries .find, .expected (the located text), .resolved (what the target actually held), and .anchor_id. It maps to the generic exit code (1) and code: "replace_verification" for MCP clients. Not retryable as-is — re-scope the replace to the cell anchor (scope=doc.anchor_by_id("table:N:R:C")), which addresses one cell at a time.

ImageSourceError

The image handed to insert_image couldn't be turned into an embeddable file: a missing or unreadable path, malformed base64, or bytes whose format isn't a recognised raster image (PNG/JPEG/GIF/BMP/TIFF). It's a bad-input error — not a missing named thing — so it maps to the generic exit code (1) rather than reusing the anchor-not-found code. Not retryable: fix the input.

SnapshotError

A page/section snapshot couldn't be rendered — almost always because the optional PDF backend (PyMuPDF) isn't installed, or because rasterising the exported PDF failed. The PDF export itself goes through Word's COM, so a busy/modal Word surfaces as WordBusyError, not this. It's an environment/dependency problem, so it maps to the generic exit code (1). Fix by installing the extra: pip install "wordlive[snapshot]" (or uv add "wordlive[snapshot]"). Not retryable until the backend is present.

WordBusyError

Word rejected the COM RPC. This usually means a modal dialog is open (Save As, Find & Replace, etc.) or Word is mid-operation. Retryable with exponential back-off. The HRESULT is on .hresult; .retryable is always True so callers can pattern-match generically.

ComError

Catch-all for any other classified COM error. Carries .hresult and .description (when pywin32 surfaces one). Not retryable in general; treat as a bug in your code or a Word-side problem.

HRESULT mapping

Only one HRESULT family is special-cased: the "Word is momentarily unavailable" codes that map to WordBusyError. Everything else becomes a generic ComError with the HRESULT preserved.

HRESULT Mnemonic wordlive exception
0x80010001 RPC_E_CALL_REJECTED WordBusyError
0x80010005 RPC_E_SERVERCALL_REJECTED WordBusyError
0x8001010A RPC_E_SERVERCALL_RETRYLATER WordBusyError
any other ComError

The classification logic lives in src/wordlive/exceptions.py:99. If you find a code that should be treated as busy/retryable, it goes in the _BUSY_HRESULTS set in that file.

CLI exit codes

The CLI maps the exception hierarchy onto six exit codes, defined in src/wordlive/cli/main.py:

Exit Exception(s) Meaning Retry?
0 success
1 WordliveError (default), DocumentNotFoundError, ImageSourceError, SnapshotError, ReplaceVerificationError other / unclassified depends on cause
2 AnchorNotFoundError, StyleNotFoundError bookmark / cc / heading / style missing, or find had zero matches yes, after re-reading content
3 WordBusyError modal dialog or busy RPC yes, with back-off
4 WordNotRunningError no Word instance only if user launches Word
5 AmbiguousMatchError replace --find matched more than one occurrence yes, after picking --occurrence N or passing --all

Retry guidance

The only exception explicitly designed to be retryable is WordBusyError. A typical retry loop:

import time
import wordlive as wl

def with_retry(fn, *, attempts=4, base=0.5):
    for i in range(attempts):
        try:
            return fn()
        except wl.WordBusyError:
            if i == attempts - 1:
                raise
            time.sleep(base * (2 ** i))   # 0.5, 1, 2, 4 seconds


def update_address():
    with wl.attach() as word:
        doc = word.documents.active
        with doc.edit("Update address"):
            doc.bookmarks["Address"].set_text("123 Main St")


with_retry(update_address)

For the CLI:

for i in 1 2 3 4; do
    wordlive write bookmark Address --text "123 Main St" && break
    rc=$?
    [ "$rc" = "3" ] || exit "$rc"      # only retry exit code 3
    sleep $((i * i))                   # quadratic-ish back-off
done

AnchorNotFoundError is also effectively retryable — but only after you've re-read outline() or the bookmark list, since the document state has demonstrably changed since your last call.