Results and errors¶
Capabilities need to report both ordinary outcomes and exceptional bugs. ZeoCore keeps those two concerns separate:
- return a
CapabilityResultfor an outcome the workflow should branch on; - raise a
ZeoErrorfor an exceptional condition outside that result contract.
The canonical imports are:
The four result constructors¶
Success: ok¶
Use success when the capability produced its intended payload:
It has status == "success", outcome == "success", typed data, no error,
and no machine message.
Intentional non-work: skip¶
Use a skip when policy or already-satisfied state says not to do the work:
It has status == "skipped" and outcome == "policy_skipped". A skip is not
an error. Its machine code must begin with ZEO_ (the compatibility prefixes
ZC_ and QC_ are also accepted).
Missing dependency: unavailable¶
Use unavailable when the declared capability cannot run because a dependency is absent:
It also has status == "skipped", but its more precise outcome is
"unavailable" and its default code is ZEO_CAP_UNAVAILABLE. This difference
lets a runner distinguish policy from missing infrastructure. Invocation
helpers produce this result automatically when declared requirements are not
available in ToolContext.
Expected operational failure: fail¶
Use failure when the capability ran but an expected operational problem prevented completion:
result = CapabilityResult.fail(
msg="Calendar API rejected the request",
code="ZEO_CALENDAR_API_ERROR",
exception=api_error,
)
It has status == "error", normally
outcome == "integration_failure", a machine code, and a structured
CapabilityError. When converting a caught exception directly,
CapabilityResult.fail_from_exc(...) is a convenience that records an
"unexpected_exception" outcome.
Branch safely¶
Status is the broad workflow branch; outcome is the precise reason:
if result.status == "success":
assert result.data is not None
print(result.data)
elif result.status == "skipped":
print(f"{result.outcome}: {result.human_message}")
else:
print(f"{result.machine_message}: {result.human_message}")
Do not assume every skipped result is a policy skip. Check result.outcome
when unavailable matters.
When to raise ZeoError¶
ZeoError and its subclasses are typed exceptions with structured context.
They are useful for configuration loaders, low-level I/O helpers, and other
APIs where returning CapabilityResult is not the function's contract:
from zeo_core.core.errors import (
ZeoError,
ZeoFileNotFoundError,
ZeoFormatError,
ZeoValidationError,
)
try:
settings = load_settings(path)
except ZeoFileNotFoundError as exc:
print(exc.context)
except ZeoFormatError as exc:
print(exc.context)
except ZeoValidationError as exc:
print(exc.context)
except ZeoError as exc:
print(f"Other Zeo error: {exc}")
See the runnable
examples/error_handling.py, which shows
missing-file, invalid-JSON, validation, and success paths. It also demonstrates
wrap_io_errors, which converts unhandled standard I/O/value exceptions into
the Zeo error family.
How invocation treats exceptions¶
Direct helper functions such as load_settings() may raise ZeoError to
their callers. A handler invoked through invoke_sync or invoke_async is a
different boundary: an exception escaping the handler is caught and converted
to an error CapabilityResult with code ZEO_CAP_UNEXPECTED.
Therefore:
- return
ok,skip,unavailable, orfailfor expected capability outcomes; - use specific
ZeoErrorsubclasses in APIs whose contract is exceptional; - do not use exceptions as routine workflow branches;
- still allow truly unexpected exceptions to surface to the invocation boundary, where ZeoCore normalizes them.
Next, learn how those handlers receive dependencies in Context, configuration, and files.