Errors & exit codes¶
pptlive 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
└── PptliveError
├── PowerPointNotRunningError
├── PresentationNotFoundError
├── AnchorNotFoundError
│ ├── SlideNotFoundError
│ └── LayoutNotFoundError
├── NoTextFrameError
├── SlideShowNotRunningError
├── UnsavedPresentationError
├── AmbiguousMatchError
├── PowerPointBusyError
└── ComError
PptliveError is the catch-all base — except pptlive.PptliveError catches
every typed error pptlive raises. Anything that wasn't a COM error in the first
place (e.g. a ValueError from your own code) bubbles up unchanged.
Reference¶
PptliveError¶
Base class. Catch this if you want one try for every pptlive failure.
PowerPointNotRunningError¶
No PowerPoint instance is running. Raised by
attach() and by
connect(launch_if_missing=False).
Not retryable within a session — PowerPoint has to actually be running.
Maps to exit 4.
PresentationNotFoundError¶
The requested presentation isn't open. Raised by
ppt.presentations[name] and by ppt.presentations.active when no deck is
active. The missing name is on .name. Maps to exit 2.
AnchorNotFoundError¶
A shape, placeholder, paragraph, table cell, notes anchor — or an entire slide
or layout — you asked for doesn't exist. .kind names the thing that was
missing ("shape", "placeholder", "paragraph", "table", "cell",
"slide", "layout", …) and .name is what you asked for. Retryable after
re-reading the slide / shape listing — the deck may have changed (and z-order
drifts). Maps to exit 2.
SlideNotFoundError¶
A slide index is out of range. Subclass of
AnchorNotFoundError, so it shares exit code 2 and
except AnchorNotFoundError catches it too. The bad index is on .index.
Retryable after re-reading deck.slides.list().
LayoutNotFoundError¶
A layout name (or index) you passed to slides.add / set_layout doesn't
exist in the deck. Subclass of AnchorNotFoundError
(exit 2). The error lists the deck's available layout names. Retryable after
reading deck.layouts() to see what's actually defined.
NoTextFrameError¶
A text operation (set_text, format_text, paragraph verbs) hit a shape with
no text frame — a picture, a line, a connector. This is the one genuinely new
code relative to wordlive: it's common enough (an LLM tries to set text on a
decorative shape) to deserve a deterministic exit code instead of a bare COM
failure. Not retryable against that shape — pick a text-bearing one. Maps
to exit 6.
SlideShowNotRunningError¶
A deck.show control verb (next, goto, black, …) was called when no
slide show is running. It's a precondition failure, not a missing anchor — so
it maps to the generic exit code (1), not 2. Start a show first with
deck.show.start(); deck.show.state() is the one verb that never raises when
nothing is running (it just reports running: false).
UnsavedPresentationError¶
deck.save() was called on a deck that has never been saved (no file path yet),
so there's nothing to save to. PowerPoint's own Save() doesn't raise here —
on a OneDrive/SharePoint build it silently uploads to the default cloud folder —
so pptlive guards on the empty path and raises instead. Use deck.save_as(path)
to give it a file first. A precondition failure, so it maps to exit 1.
AmbiguousMatchError¶
A fuzzy match resolved to more than one target without disambiguation. The exception carries the candidates so an agent can pick one and retry. Retryable by narrowing the request. Maps to exit 5.
PowerPointBusyError¶
PowerPoint rejected the COM RPC — usually a modal dialog is open (Save As,
Insert, a Format pane prompt) or the app is mid-operation. Retryable with
exponential back-off. The HRESULT is on .hresult; .retryable is always
True. Maps to exit 3.
A running slide show does not block edits
The spec originally assumed editing during a live show would reject as
busy. A 2026 spike overturned that: a set_text mid-show succeeds and
raises nothing. So a running show is not, by itself, a PowerPointBusyError
— this exception stays the home for genuine modal-dialog RPC_E_*
rejections.
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 PowerPoint-side problem. Maps to exit 1.
HRESULT mapping¶
Only one HRESULT family is special-cased: the "PowerPoint is momentarily
unavailable" codes that map to PowerPointBusyError.
Everything else becomes a generic ComError with the HRESULT
preserved.
| HRESULT | Mnemonic | pptlive exception |
|---|---|---|
0x80010001 |
RPC_E_CALL_REJECTED |
PowerPointBusyError |
0x80010005 |
RPC_E_SERVERCALL_REJECTED |
PowerPointBusyError |
0x8001010A |
RPC_E_SERVERCALL_RETRYLATER |
PowerPointBusyError |
| any other | — | ComError |
The classification logic lives in the _BUSY_HRESULTS set in
src/pptlive/exceptions.py.
If you find a code that should be treated as busy/retryable, it goes there.
CLI exit codes¶
The CLI maps the exception hierarchy onto seven exit codes, defined in
src/pptlive/cli/main.py:
| Exit | Exception(s) | Meaning | Retry? |
|---|---|---|---|
0 |
— | success | — |
1 |
PptliveError (default), SlideShowNotRunningError, UnsavedPresentationError, ComError |
other / unclassified / no show running / never-saved deck | depends on cause |
2 |
AnchorNotFoundError, SlideNotFoundError, LayoutNotFoundError, PresentationNotFoundError |
anchor / slide / shape / layout / deck missing | yes, after re-reading content |
3 |
PowerPointBusyError |
modal dialog or busy RPC | yes, with back-off |
4 |
PowerPointNotRunningError |
no PowerPoint instance | only if the user launches PowerPoint |
5 |
AmbiguousMatchError |
fuzzy match hit more than one target | yes, after disambiguating |
6 |
NoTextFrameError |
text op on a shape with no text frame | no — pick a text-bearing shape |
Retry guidance¶
The only exception explicitly designed to be retryable is
PowerPointBusyError. A typical retry loop:
import time
import pptlive as pl
def with_retry(fn, *, attempts=4, base=0.5):
for i in range(attempts):
try:
return fn()
except pl.PowerPointBusyError:
if i == attempts - 1:
raise
time.sleep(base * (2 ** i)) # 0.5, 1, 2, 4 seconds
def update_title():
with pl.attach() as ppt:
deck = ppt.presentations.active
with deck.edit("Update title"):
deck.anchor_by_id("ph:1:title").set_text("Q3 Results")
with_retry(update_title)
For the CLI (PowerShell):
foreach ($i in 1..4) {
pptlive write --anchor-id ph:1:title --text "Q3 Results"
if ($LASTEXITCODE -eq 0) { break }
if ($LASTEXITCODE -ne 3) { exit $LASTEXITCODE } # only retry exit code 3
Start-Sleep -Seconds ($i * $i) # quadratic-ish back-off
}
AnchorNotFoundError is also effectively retryable —
but only after you've re-read the slide / shape listing, since z-order drifts
and the deck may have changed since your last call.