Error Handling¶
All exceptions in systemd-client inherit from SystemdClientError, so you can catch everything with a single handler -- or be as specific as you need.
Let's walk through the exception hierarchy and see how to handle each one.
Exception Hierarchy¶
graph TD
BASE["SystemdClientError"]
UNF["UnitNotFoundError"]
UOP["UnitOperationError"]
BE["BackendError"]
BNA["BackendNotAvailableError"]
SPE["SubprocessError"]
JE["JournalError"]
JPE["JournalParseError"]
BASE --> UNF
BASE --> UOP
BASE --> BE
BE --> BNA
BE --> SPE
BASE --> JE
JE --> JPE
style BASE fill:#ffe3e3,stroke:#c92a2a,stroke-width:2px
style UNF fill:#fff4e6,stroke:#e8590c,stroke-width:2px
style UOP fill:#fff4e6,stroke:#e8590c,stroke-width:2px
style BE fill:#f3d9fa,stroke:#9c36b5,stroke-width:2px
style BNA fill:#f3d9fa,stroke:#9c36b5,stroke-width:2px
style SPE fill:#f3d9fa,stroke:#9c36b5,stroke-width:2px
style JE fill:#d0ebff,stroke:#1971c2,stroke-width:2px
style JPE fill:#d0ebff,stroke:#1971c2,stroke-width:2px
Info
The hierarchy is designed so you can catch at any level. Catch SystemdClientError for a safety net, or catch specific exceptions for fine-grained control.
Catch-All Handler¶
The simplest approach -- catch SystemdClientError and handle everything in one place:
from systemd_client import SystemdClient, SystemdClientError
client = SystemdClient()
try:
client.restart("my-app.service")
except SystemdClientError as e:
print(f"Something went wrong: {e}") # (1)!
- Every exception in the library inherits from this, so nothing escapes.
Tip
This is great for scripts where you just want to log the error and move on. For production code, prefer catching specific exceptions so you can handle each case appropriately.
Specific Exceptions¶
UnitNotFoundError¶
Raised when a unit doesn't exist:
from systemd_client import UnitNotFoundError
try:
status = client.status("nonexistent.service")
except UnitNotFoundError as e:
print(f"Unit not found: {e.unit_name}") # (1)!
- The
unit_nameattribute tells you exactly which unit was missing.
| Attribute | Type | Description |
|---|---|---|
unit_name |
str |
The name of the unit that wasn't found |
UnitOperationError¶
Raised when start/stop/restart/enable/disable/mask/unmask fails:
from systemd_client import UnitOperationError
try:
client.start("broken.service")
except UnitOperationError as e:
print(f"Failed to {e.operation} {e.unit_name}: {e.detail}") # (1)!
- Gives you the operation that failed, the unit it was targeting, and a human-readable explanation.
| Attribute | Type | Description |
|---|---|---|
unit_name |
str |
The target unit |
operation |
str |
What was attempted ("start", "stop", etc.) |
detail |
str |
Human-readable error message from systemd |
Note
This means the unit exists but the operation couldn't complete. For missing units, you'll get UnitNotFoundError instead.
BackendNotAvailableError¶
Raised when you request a backend that can't be initialized:
from systemd_client import BackendNotAvailableError, BackendType
try:
client = SystemdClient(backend=BackendType.DBUS)
except BackendNotAvailableError as e:
print(f"Backend {e.backend} not available: {e.reason}") # (1)!
- Common reasons:
dasbusnot installed, D-Bus session bus not accessible.
| Attribute | Type | Description |
|---|---|---|
backend |
str |
Which backend was requested |
reason |
str |
Why it couldn't be initialized |
Other options to avoid this error...
Use BackendType.AUTO (the default) and the library will silently fall back to subprocess if D-Bus isn't available:
SubprocessError¶
Raised when a subprocess command exits with an unexpected error:
from systemd_client import SubprocessError
try:
client.daemon_reload()
except SubprocessError as e:
print(f"Command failed: {' '.join(e.command)}")
print(f"Exit code: {e.returncode}")
print(f"Stderr: {e.stderr}") # (1)!
- The
stderroutput usually contains the actual error message fromsystemctl.
| Attribute | Type | Description |
|---|---|---|
command |
list[str] |
The full command that was executed |
returncode |
int |
Process exit code |
stderr |
str |
Standard error output |
Warning
This is a low-level error from the subprocess backend. In most cases, you'll see a higher-level UnitOperationError or UnitNotFoundError instead. SubprocessError surfaces when something truly unexpected happens.
JournalParseError¶
Raised when journal output can't be parsed as JSON:
from systemd_client import JournalParseError
try:
entries = client.journal("my-app.service")
except JournalParseError as e:
print(f"Parse error: {e.detail}") # (1)!
- This is rare -- it typically means
journalctlreturned something unexpected.
| Attribute | Type | Description |
|---|---|---|
detail |
str |
What went wrong during parsing |
Pattern: Graceful Degradation¶
Now let's put it all together. Here's a real-world pattern that handles errors gracefully:
from systemd_client import (
SystemdClient,
UnitNotFoundError,
UnitOperationError,
)
def safe_restart(unit_name: str) -> bool:
client = SystemdClient()
try:
client.restart(unit_name)
return True
except UnitNotFoundError:
print(f"Unit {unit_name} not found -- skipping") # (1)!
return False
except UnitOperationError as e:
print(f"Restart failed: {e.detail}") # (2)!
return False
- The unit doesn't exist. Maybe it hasn't been installed yet -- not necessarily a fatal error.
- The unit exists but couldn't be restarted. Log the details and let the caller decide what to do.
Check
With this pattern, your code handles both "unit missing" and "unit broken" cases without crashing.
Other options for error handling strategies...
You can also combine the catch-all with specific handlers:
from systemd_client import SystemdClientError, UnitNotFoundError
try:
client.restart(unit_name)
except UnitNotFoundError:
return "not_found" # Handle specifically
except SystemdClientError:
return "error" # Catch everything else
Python tries except blocks top-to-bottom, so put specific exceptions before the general ones.