[](https://pypi.org/project/st-error-boundary/)
[](https://pypi.org/project/st-error-boundary/)
[](https://pepy.tech/projects/st-error-boundary)
[](https://codecov.io/gh/K-dash/st-error-boundary)
# st-error-boundary
English | [日本語](README.ja.md)
A minimal, type-safe error boundary library for Streamlit applications with pluggable hooks and safe fallback UI.
## Motivation
Streamlit's default behavior displays detailed stack traces in the browser when exceptions occur. While `client.showErrorDetails = "none"` prevents information leakage, it shows only generic error messages, leaving users confused. The typical solution—scattering `st.error()` and `st.stop()` calls throughout your code—**severely degrades readability and maintainability**, and creates a risk of **forgetting exception handling** in critical places.
This library solves the problem with the **decorator pattern**: a single "last line of defense" decorator that separates exception handling (cross-cutting concern) from business logic. Just decorate your main function, and all unhandled exceptions are caught and displayed with user-friendly messages—no need to pollute your code with error handling boilerplate everywhere.
This pattern is extracted from production use and open-sourced to help others build robust Streamlit applications without sacrificing code clarity. For the full architectural context, see the [PyConJP 2025 presentation](https://speakerdeck.com/kdash/streamlit-hashe-nei-turudakeziyanai-poc-nosu-sadeshi-xian-surushang-yong-pin-zhi-nofen-xi-saas-akitekutiya).
In **customer-facing and regulated** environments, an unhandled exception that leaks internals isn’t just noisy—it can be a business incident. You want **no stack traces in the UI**, but **rich, sanitized telemetry** behind the scenes.
## Who is this for?
Teams shipping **customer-facing** Streamlit apps (B2B/B2C, regulated or enterprise settings) where you want **no stack traces in the UI**, but **rich telemetry** in your logs/alerts. The boundary provides a **consistent, user-friendly fallback** while `on_error` sends **sanitized** details to your observability stack.
## Features
- **Minimal API**: Just two required arguments (`on_error` and `fallback`)
- **PEP 561 Compatible**: Ships with `py.typed` for full type checker support
- **Callback Protection**: Protect both decorated functions and widget callbacks (`on_click`, `on_change`, etc.)
- **Pluggable Hooks**: Execute side effects (audit logging, metrics, notifications) when errors occur
- **Safe Fallback UI**: Display user-friendly error messages instead of tracebacks
## Installation
```bash
pip install st-error-boundary
```
## Quick Start
### Basic Usage (Decorator Only)
For simple cases where you only need to protect the main function:
```python
import streamlit as st
from st_error_boundary import ErrorBoundary
# Create error boundary
boundary = ErrorBoundary(
on_error=lambda exc: print(f"Error logged: {exc}"),
fallback="An error occurred. Please try again later."
)
@boundary.decorate
def main() -> None:
st.title("My App")
if st.button("Trigger Error"):
raise ValueError("Something went wrong")
if __name__ == "__main__":
main()
```
**⚠️ Important**: The `@boundary.decorate` decorator alone does **not** protect `on_click`/`on_change` callbacks—you must use `boundary.wrap_callback()` for those (see Advanced Usage below).
### Advanced Usage (With Callbacks)
To protect both decorated functions **and** widget callbacks:
```python
import streamlit as st
from st_error_boundary import ErrorBoundary
def audit_log(exc: Exception) -> None:
# Log to monitoring service
print(f"Error: {exc}")
def fallback_ui(exc: Exception) -> None:
st.error("An unexpected error occurred.")
st.link_button("Contact Support", "https://example.com/support")
if st.button("Retry"):
st.rerun()
# Single ErrorBoundary instance for DRY configuration
boundary = ErrorBoundary(on_error=audit_log, fallback=fallback_ui)
def handle_click() -> None:
# This will raise an error
result = 1 / 0
@boundary.decorate
def main() -> None:
st.title("My App")
# Protected: error in if statement
if st.button("Direct Error"):
raise ValueError("Error in main function")
# Protected: error in callback
st.button("Callback Error", on_click=boundary.wrap_callback(handle_click))
if __name__ == "__main__":
main()
```
## Comparison: Traditional try/except vs ErrorBoundary (raise-only)
### Traditional: scatter try/except + `st.error()` / `st.stop()` in each screen
```python
from __future__ import annotations
import streamlit as st
def save_profile(name: str) -> None:
if not name:
raise ValueError("Name is required")
# ... persist ...
def main() -> None:
st.title("Profile")
name: str = st.text_input("Name", "")
if st.button("Save"):
try:
save_profile(name)
st.success("Saved!")
except ValueError as exc:
st.error(f"Input error: {exc}")
st.stop() # stop to avoid broken UI after errors
if __name__ == "__main__":
main()
```
* Drawbacks: exception handling is **duplicated and easy to forget**, readability suffers, and a cross-cutting concern leaks into business logic.
### ErrorBoundary: just **raise**, and let the boundary handle UI + hooks
```python
from __future__ import annotations
import streamlit as st
from st_error_boundary import ErrorBoundary
def audit_log(exc: Exception) -> None:
# send to your telemetry/metrics
print(f"[audit] {exc!r}")
def fallback_ui(exc: Exception) -> None:
st.error("An unexpected error occurred. Please try again later.")
if st.button("Retry"):
st.rerun()
boundary: ErrorBoundary = ErrorBoundary(
on_error=audit_log,
fallback=fallback_ui, # string is also allowed; it is rendered via st.error() internally
)
def save_profile(name: str) -> None:
if not name:
raise ValueError("Name is required")
# ... persist ...
@boundary.decorate
def main() -> None:
st.title("Profile")
name: str = st.text_input("Name", "")
if st.button("Save"):
# No local try/except: domain errors bubble to the boundary
save_profile(name)
if __name__ == "__main__":
main()
```
* Benefits: **single place** for error UI and side effects; business code stays clean. When `fallback` is a string, it is shown with `st.error()` internally (use a callable for custom UI).
* Callbacks (`on_click` / `on_change`) run outside the decorated scope—wrap them with `boundary.wrap_callback(...)`.
* Control-flow exceptions (`st.rerun()`, `st.stop()`) **pass through** boundaries; keep using them intentionally as needed (you don't need `st.stop()` as error handling boilerplate).
### When to still use local try/except
* You want to **recover inline** (e.g., fix input and continue).
* You need **fine-grained branching UI** for a specific, expected exception.
* You implement a **local retry** for an external API and intentionally swallow the exception.
## Why ErrorBoundary Class?
Streamlit executes `on_click` and `on_change` callbacks **before** the script reruns, meaning they run **outside** the decorated function's scope. This is why `@boundary.decorate` alone cannot catch callback errors.
**Execution Flow:**
1. User clicks button with `on_click=callback`
2. Streamlit executes `callback()` -> **Not protected by decorator**
3. Streamlit reruns the script
4. Decorated function executes -> **Protected by decorator**
**Solution**: Use `boundary.wrap_callback()` to explicitly wrap callbacks with the same error handling logic.
## API Reference
### `ErrorBoundary`
```python
ErrorBoundary(
on_error: ErrorHook | Iterable[ErrorHook],
fallback: str | FallbackRenderer
)
```
**Parameters:**
- `on_error`: Single hook or list of hooks for side effects (logging, metrics, etc.)
- `fallback`: Either a string (displayed via `st.error()`) or a callable that renders custom UI
- When `fallback` is a `str`, it is rendered using `st.error()` internally
- To customize rendering (e.g., use `st.warning()` or custom widgets), pass a `FallbackRenderer` callable instead
**Methods:**
- `.decorate(func)`: Decorator to wrap a function with error boundary
- `.wrap_callback(callback)`: Wrap a widget callback (on_click, on_change, etc.)
### `ErrorHook` Protocol
```python
def hook(exc: Exception) -> None:
"""Handle exception with side effects."""
...
```
### `FallbackRenderer` Protocol
```python
def renderer(exc: Exception) -> None:
"""Render fallback UI for the exception."""
...
```
## Examples
### Multiple Hooks
```python
def log_error(exc: Exception) -> None:
logging.error(f"Error: {exc}")
def send_metric(exc: Exception) -> None:
metrics.increment("app.errors")
boundary = ErrorBoundary(
on_error=[log_error, send_metric], # Hooks execute in order
fallback="An error occurred."
)
```
### Custom Fallback UI
```python
def custom_fallback(exc: Exception) -> None:
st.error(f"Error: {type(exc).__name__}")
st.warning("Please try again or contact support.")
col1, col2 = st.columns(2)
with col1:
if st.button("Retry"):
st.rerun()
with col2:
st.link_button("Report Bug", "https://example.com/bug-report")
boundary = ErrorBoundary(on_error=lambda _: None, fallback=custom_fallback)
```
## Important Notes
### Callback Error Rendering Position
**TL;DR**: Errors in callbacks appear at the top of the page, not near the widget. Use the deferred rendering pattern (below) to control error position.
When using `wrap_callback()`, errors in widget callbacks (`on_click`, `on_change`) are rendered at the **top of the page** instead of near the widget. This is a Streamlit architectural limitation.
#### Deferred Rendering Pattern
Store errors in `session_state` during callback execution, then render them during main script execution:
```python
import streamlit as st
from st_error_boundary import ErrorBoundary
# Initialize session state
if "error" not in st.session_state:
st.session_state.error = None
# Store error instead of rendering it
boundary = ErrorBoundary(
on_error=lambda exc: st.session_state.update(error=str(exc)),
fallback=lambda _: None # Silent - defer to main script
)
def trigger_error():
raise ValueError("Error in callback!")
# Main app
st.button("Click", on_click=boundary.wrap_callback(trigger_error))
# Render error after the button
if st.session_state.error:
st.error(f"Error: {st.session_state.error}")
if st.button("Clear"):
st.session_state.error = None
st.rerun()
```
**Result**: Error appears **below the button** instead of at the top.
For more details, see [Callback Rendering Position Guide](docs/callback-rendering-position.md).
### Nested ErrorBoundary Behavior
When `ErrorBoundary` instances are nested (hierarchical), the following rules apply:
1. **Inner boundary handles first** (first-match wins)
- The innermost boundary that catches the exception handles it.
2. **Only inner hooks execute**
- When the inner boundary handles an exception, **only the inner boundary's hooks are called**. Outer boundary hooks are NOT executed.
3. **Fallback exceptions bubble up**
- If the inner boundary's fallback raises an exception, that exception propagates to the outer boundary. The outer boundary then handles it (by design, fallback bugs are not silently ignored).
4. **Control flow exceptions pass through**
- Streamlit control flow exceptions (`st.rerun()`, `st.stop()`) pass through **all** boundaries without being caught.
5. **Same rules for callbacks**
- `wrap_callback()` follows the same nesting rules—the innermost boundary wrapping the callback handles exceptions.
#### Example: Inner Boundary Handles
```python
outer = ErrorBoundary(on_error=outer_hook, fallback="OUTER")
inner = ErrorBoundary(on_error=inner_hook, fallback="INNER")
@outer.decorate
def main():
@inner.decorate
def section():
raise ValueError("boom")
section()
```
**Result**:
- `INNER` fallback is displayed
- Only `inner_hook` is called (not `outer_hook`)
#### Example: Fallback Exception Bubbles
```python
def bad_fallback(exc: Exception):
raise RuntimeError("fallback failed")
outer = ErrorBoundary(on_error=outer_hook, fallback="OUTER")
inner = ErrorBoundary(on_error=inner_hook, fallback=bad_fallback)
@outer.decorate
def main():
@inner.decorate
def section():
raise ValueError("boom")
section()
```
**Result**:
- `OUTER` fallback is displayed (inner fallback raised exception)
- Both `inner_hook` and `outer_hook` are called (inner first, then outer)
#### Best Practice
- **Inner fallback**: Render UI and finish (don't raise). This keeps errors isolated.
- **Outer fallback**: If you want outer boundaries to handle certain errors, explicitly `raise` from the inner fallback.
#### Test Coverage
All nested boundary behaviors are verified by automated tests.
See [`tests/test_integration.py`](tests/test_integration.py) for implementation details.
## Development
```bash
# Install dependencies
make install
# Install pre-commit hooks (recommended)
make install-hooks
# Run linting and type checking
make
# Run tests
make test
# Run example app
make example
# Run demo
make demo
```
### Pre-commit Hooks
This project uses [pre-commit](https://pre-commit.com/) to automatically run code quality checks before each commit:
- **Code Formatting**: ruff format
- **Linting**: ruff check
- **Type Checking**: mypy and pyright
- **Tests**: pytest
- **Other Checks**: trailing whitespace, end-of-file, YAML/TOML validation
**Setup:**
```bash
# Install pre-commit hooks (one-time setup)
make install-hooks
```
After installation, the hooks will run automatically on `git commit`. To run manually:
```bash
# Run on all files
uv run pre-commit run --all-files
# Skip hooks for a specific commit (not recommended)
git commit --no-verify
```
## License
MIT
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Raw data
{
"_id": null,
"home_page": null,
"name": "st-error-boundary",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.12",
"maintainer_email": null,
"keywords": "decorator, error-boundary, error-handling, streamlit",
"author": "K-dash",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/76/88/3935e6bc245c89186a4798d3bc60b4f81a4ca4d4dcd0a24f38caf55adbf3/st_error_boundary-0.1.8.tar.gz",
"platform": null,
"description": "[](https://pypi.org/project/st-error-boundary/)\n[](https://pypi.org/project/st-error-boundary/)\n[](https://pepy.tech/projects/st-error-boundary)\n[](https://codecov.io/gh/K-dash/st-error-boundary)\n\n# st-error-boundary\n\nEnglish | [\u65e5\u672c\u8a9e](README.ja.md)\n\nA minimal, type-safe error boundary library for Streamlit applications with pluggable hooks and safe fallback UI.\n\n## Motivation\n\nStreamlit's default behavior displays detailed stack traces in the browser when exceptions occur. While `client.showErrorDetails = \"none\"` prevents information leakage, it shows only generic error messages, leaving users confused. The typical solution\u2014scattering `st.error()` and `st.stop()` calls throughout your code\u2014**severely degrades readability and maintainability**, and creates a risk of **forgetting exception handling** in critical places.\n\nThis library solves the problem with the **decorator pattern**: a single \"last line of defense\" decorator that separates exception handling (cross-cutting concern) from business logic. Just decorate your main function, and all unhandled exceptions are caught and displayed with user-friendly messages\u2014no need to pollute your code with error handling boilerplate everywhere.\n\nThis pattern is extracted from production use and open-sourced to help others build robust Streamlit applications without sacrificing code clarity. For the full architectural context, see the [PyConJP 2025 presentation](https://speakerdeck.com/kdash/streamlit-hashe-nei-turudakeziyanai-poc-nosu-sadeshi-xian-surushang-yong-pin-zhi-nofen-xi-saas-akitekutiya).\n\nIn **customer-facing and regulated** environments, an unhandled exception that leaks internals isn\u2019t just noisy\u2014it can be a business incident. You want **no stack traces in the UI**, but **rich, sanitized telemetry** behind the scenes.\n\n## Who is this for?\n\nTeams shipping **customer-facing** Streamlit apps (B2B/B2C, regulated or enterprise settings) where you want **no stack traces in the UI**, but **rich telemetry** in your logs/alerts. The boundary provides a **consistent, user-friendly fallback** while `on_error` sends **sanitized** details to your observability stack.\n\n\n## Features\n\n- **Minimal API**: Just two required arguments (`on_error` and `fallback`)\n- **PEP 561 Compatible**: Ships with `py.typed` for full type checker support\n- **Callback Protection**: Protect both decorated functions and widget callbacks (`on_click`, `on_change`, etc.)\n- **Pluggable Hooks**: Execute side effects (audit logging, metrics, notifications) when errors occur\n- **Safe Fallback UI**: Display user-friendly error messages instead of tracebacks\n\n## Installation\n\n```bash\npip install st-error-boundary\n```\n\n## Quick Start\n\n### Basic Usage (Decorator Only)\n\nFor simple cases where you only need to protect the main function:\n\n```python\nimport streamlit as st\nfrom st_error_boundary import ErrorBoundary\n\n# Create error boundary\nboundary = ErrorBoundary(\n on_error=lambda exc: print(f\"Error logged: {exc}\"),\n fallback=\"An error occurred. Please try again later.\"\n)\n\n@boundary.decorate\ndef main() -> None:\n st.title(\"My App\")\n\n if st.button(\"Trigger Error\"):\n raise ValueError(\"Something went wrong\")\n\nif __name__ == \"__main__\":\n main()\n```\n\n**\u26a0\ufe0f Important**: The `@boundary.decorate` decorator alone does **not** protect `on_click`/`on_change` callbacks\u2014you must use `boundary.wrap_callback()` for those (see Advanced Usage below).\n\n### Advanced Usage (With Callbacks)\n\nTo protect both decorated functions **and** widget callbacks:\n\n```python\nimport streamlit as st\nfrom st_error_boundary import ErrorBoundary\n\ndef audit_log(exc: Exception) -> None:\n # Log to monitoring service\n print(f\"Error: {exc}\")\n\ndef fallback_ui(exc: Exception) -> None:\n st.error(\"An unexpected error occurred.\")\n st.link_button(\"Contact Support\", \"https://example.com/support\")\n if st.button(\"Retry\"):\n st.rerun()\n\n# Single ErrorBoundary instance for DRY configuration\nboundary = ErrorBoundary(on_error=audit_log, fallback=fallback_ui)\n\ndef handle_click() -> None:\n # This will raise an error\n result = 1 / 0\n\n@boundary.decorate\ndef main() -> None:\n st.title(\"My App\")\n\n # Protected: error in if statement\n if st.button(\"Direct Error\"):\n raise ValueError(\"Error in main function\")\n\n # Protected: error in callback\n st.button(\"Callback Error\", on_click=boundary.wrap_callback(handle_click))\n\nif __name__ == \"__main__\":\n main()\n```\n\n## Comparison: Traditional try/except vs ErrorBoundary (raise-only)\n\n### Traditional: scatter try/except + `st.error()` / `st.stop()` in each screen\n\n```python\nfrom __future__ import annotations\nimport streamlit as st\n\ndef save_profile(name: str) -> None:\n if not name:\n raise ValueError(\"Name is required\")\n # ... persist ...\n\ndef main() -> None:\n st.title(\"Profile\")\n name: str = st.text_input(\"Name\", \"\")\n if st.button(\"Save\"):\n try:\n save_profile(name)\n st.success(\"Saved!\")\n except ValueError as exc:\n st.error(f\"Input error: {exc}\")\n st.stop() # stop to avoid broken UI after errors\n\nif __name__ == \"__main__\":\n main()\n```\n\n* Drawbacks: exception handling is **duplicated and easy to forget**, readability suffers, and a cross-cutting concern leaks into business logic.\n\n### ErrorBoundary: just **raise**, and let the boundary handle UI + hooks\n\n```python\nfrom __future__ import annotations\nimport streamlit as st\nfrom st_error_boundary import ErrorBoundary\n\ndef audit_log(exc: Exception) -> None:\n # send to your telemetry/metrics\n print(f\"[audit] {exc!r}\")\n\ndef fallback_ui(exc: Exception) -> None:\n st.error(\"An unexpected error occurred. Please try again later.\")\n if st.button(\"Retry\"):\n st.rerun()\n\nboundary: ErrorBoundary = ErrorBoundary(\n on_error=audit_log,\n fallback=fallback_ui, # string is also allowed; it is rendered via st.error() internally\n)\n\ndef save_profile(name: str) -> None:\n if not name:\n raise ValueError(\"Name is required\")\n # ... persist ...\n\n@boundary.decorate\ndef main() -> None:\n st.title(\"Profile\")\n name: str = st.text_input(\"Name\", \"\")\n if st.button(\"Save\"):\n # No local try/except: domain errors bubble to the boundary\n save_profile(name)\n\nif __name__ == \"__main__\":\n main()\n```\n\n* Benefits: **single place** for error UI and side effects; business code stays clean. When `fallback` is a string, it is shown with `st.error()` internally (use a callable for custom UI).\n* Callbacks (`on_click` / `on_change`) run outside the decorated scope\u2014wrap them with `boundary.wrap_callback(...)`.\n* Control-flow exceptions (`st.rerun()`, `st.stop()`) **pass through** boundaries; keep using them intentionally as needed (you don't need `st.stop()` as error handling boilerplate).\n\n### When to still use local try/except\n\n* You want to **recover inline** (e.g., fix input and continue).\n* You need **fine-grained branching UI** for a specific, expected exception.\n* You implement a **local retry** for an external API and intentionally swallow the exception.\n\n## Why ErrorBoundary Class?\n\nStreamlit executes `on_click` and `on_change` callbacks **before** the script reruns, meaning they run **outside** the decorated function's scope. This is why `@boundary.decorate` alone cannot catch callback errors.\n\n**Execution Flow:**\n1. User clicks button with `on_click=callback`\n2. Streamlit executes `callback()` -> **Not protected by decorator**\n3. Streamlit reruns the script\n4. Decorated function executes -> **Protected by decorator**\n\n**Solution**: Use `boundary.wrap_callback()` to explicitly wrap callbacks with the same error handling logic.\n\n## API Reference\n\n### `ErrorBoundary`\n\n```python\nErrorBoundary(\n on_error: ErrorHook | Iterable[ErrorHook],\n fallback: str | FallbackRenderer\n)\n```\n\n**Parameters:**\n- `on_error`: Single hook or list of hooks for side effects (logging, metrics, etc.)\n- `fallback`: Either a string (displayed via `st.error()`) or a callable that renders custom UI\n - When `fallback` is a `str`, it is rendered using `st.error()` internally\n - To customize rendering (e.g., use `st.warning()` or custom widgets), pass a `FallbackRenderer` callable instead\n\n**Methods:**\n- `.decorate(func)`: Decorator to wrap a function with error boundary\n- `.wrap_callback(callback)`: Wrap a widget callback (on_click, on_change, etc.)\n\n### `ErrorHook` Protocol\n\n```python\ndef hook(exc: Exception) -> None:\n \"\"\"Handle exception with side effects.\"\"\"\n ...\n```\n\n### `FallbackRenderer` Protocol\n\n```python\ndef renderer(exc: Exception) -> None:\n \"\"\"Render fallback UI for the exception.\"\"\"\n ...\n```\n\n## Examples\n\n### Multiple Hooks\n\n```python\ndef log_error(exc: Exception) -> None:\n logging.error(f\"Error: {exc}\")\n\ndef send_metric(exc: Exception) -> None:\n metrics.increment(\"app.errors\")\n\nboundary = ErrorBoundary(\n on_error=[log_error, send_metric], # Hooks execute in order\n fallback=\"An error occurred.\"\n)\n```\n\n### Custom Fallback UI\n\n```python\ndef custom_fallback(exc: Exception) -> None:\n st.error(f\"Error: {type(exc).__name__}\")\n st.warning(\"Please try again or contact support.\")\n\n col1, col2 = st.columns(2)\n with col1:\n if st.button(\"Retry\"):\n st.rerun()\n with col2:\n st.link_button(\"Report Bug\", \"https://example.com/bug-report\")\n\nboundary = ErrorBoundary(on_error=lambda _: None, fallback=custom_fallback)\n```\n\n## Important Notes\n\n### Callback Error Rendering Position\n\n**TL;DR**: Errors in callbacks appear at the top of the page, not near the widget. Use the deferred rendering pattern (below) to control error position.\n\nWhen using `wrap_callback()`, errors in widget callbacks (`on_click`, `on_change`) are rendered at the **top of the page** instead of near the widget. This is a Streamlit architectural limitation.\n\n#### Deferred Rendering Pattern\n\nStore errors in `session_state` during callback execution, then render them during main script execution:\n\n```python\nimport streamlit as st\nfrom st_error_boundary import ErrorBoundary\n\n# Initialize session state\nif \"error\" not in st.session_state:\n st.session_state.error = None\n\n# Store error instead of rendering it\nboundary = ErrorBoundary(\n on_error=lambda exc: st.session_state.update(error=str(exc)),\n fallback=lambda _: None # Silent - defer to main script\n)\n\ndef trigger_error():\n raise ValueError(\"Error in callback!\")\n\n# Main app\nst.button(\"Click\", on_click=boundary.wrap_callback(trigger_error))\n\n# Render error after the button\nif st.session_state.error:\n st.error(f\"Error: {st.session_state.error}\")\n if st.button(\"Clear\"):\n st.session_state.error = None\n st.rerun()\n```\n\n**Result**: Error appears **below the button** instead of at the top.\n\nFor more details, see [Callback Rendering Position Guide](docs/callback-rendering-position.md).\n\n### Nested ErrorBoundary Behavior\n\nWhen `ErrorBoundary` instances are nested (hierarchical), the following rules apply:\n\n1. **Inner boundary handles first** (first-match wins)\n - The innermost boundary that catches the exception handles it.\n\n2. **Only inner hooks execute**\n - When the inner boundary handles an exception, **only the inner boundary's hooks are called**. Outer boundary hooks are NOT executed.\n\n3. **Fallback exceptions bubble up**\n - If the inner boundary's fallback raises an exception, that exception propagates to the outer boundary. The outer boundary then handles it (by design, fallback bugs are not silently ignored).\n\n4. **Control flow exceptions pass through**\n - Streamlit control flow exceptions (`st.rerun()`, `st.stop()`) pass through **all** boundaries without being caught.\n\n5. **Same rules for callbacks**\n - `wrap_callback()` follows the same nesting rules\u2014the innermost boundary wrapping the callback handles exceptions.\n\n#### Example: Inner Boundary Handles\n\n```python\nouter = ErrorBoundary(on_error=outer_hook, fallback=\"OUTER\")\ninner = ErrorBoundary(on_error=inner_hook, fallback=\"INNER\")\n\n@outer.decorate\ndef main():\n @inner.decorate\n def section():\n raise ValueError(\"boom\")\n section()\n```\n\n**Result**:\n- `INNER` fallback is displayed\n- Only `inner_hook` is called (not `outer_hook`)\n\n#### Example: Fallback Exception Bubbles\n\n```python\ndef bad_fallback(exc: Exception):\n raise RuntimeError(\"fallback failed\")\n\nouter = ErrorBoundary(on_error=outer_hook, fallback=\"OUTER\")\ninner = ErrorBoundary(on_error=inner_hook, fallback=bad_fallback)\n\n@outer.decorate\ndef main():\n @inner.decorate\n def section():\n raise ValueError(\"boom\")\n section()\n```\n\n**Result**:\n- `OUTER` fallback is displayed (inner fallback raised exception)\n- Both `inner_hook` and `outer_hook` are called (inner first, then outer)\n\n#### Best Practice\n\n- **Inner fallback**: Render UI and finish (don't raise). This keeps errors isolated.\n- **Outer fallback**: If you want outer boundaries to handle certain errors, explicitly `raise` from the inner fallback.\n\n#### Test Coverage\n\nAll nested boundary behaviors are verified by automated tests.\nSee [`tests/test_integration.py`](tests/test_integration.py) for implementation details.\n\n## Development\n\n```bash\n# Install dependencies\nmake install\n\n# Install pre-commit hooks (recommended)\nmake install-hooks\n\n# Run linting and type checking\nmake\n\n# Run tests\nmake test\n\n# Run example app\nmake example\n\n# Run demo\nmake demo\n```\n\n### Pre-commit Hooks\n\nThis project uses [pre-commit](https://pre-commit.com/) to automatically run code quality checks before each commit:\n\n- **Code Formatting**: ruff format\n- **Linting**: ruff check\n- **Type Checking**: mypy and pyright\n- **Tests**: pytest\n- **Other Checks**: trailing whitespace, end-of-file, YAML/TOML validation\n\n**Setup:**\n\n```bash\n# Install pre-commit hooks (one-time setup)\nmake install-hooks\n```\n\nAfter installation, the hooks will run automatically on `git commit`. To run manually:\n\n```bash\n# Run on all files\nuv run pre-commit run --all-files\n\n# Skip hooks for a specific commit (not recommended)\ngit commit --no-verify\n```\n\n## License\n\nMIT\n\n## Contributing\n\nContributions are welcome! Please open an issue or submit a pull request.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A tiny, typed error-boundary decorator for Streamlit apps (UI-safe fallback + pluggable hooks)",
"version": "0.1.8",
"project_urls": {
"Homepage": "https://github.com/K-dash/st-error-boundary",
"Repository": "https://github.com/K-dash/st-error-boundary"
},
"split_keywords": [
"decorator",
" error-boundary",
" error-handling",
" streamlit"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "23d04744f8ffebfbbb9fac55fdcd82f172ee19dd8af70d13828062a63f364894",
"md5": "a672ac41f5170777dcac0ac98a722a7d",
"sha256": "cb3a7978465cca61ec925495f19e1cba5fc35b5e17fc8735f69e1f305e0f3c1a"
},
"downloads": -1,
"filename": "st_error_boundary-0.1.8-py3-none-any.whl",
"has_sig": false,
"md5_digest": "a672ac41f5170777dcac0ac98a722a7d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.12",
"size": 9865,
"upload_time": "2025-10-24T00:17:01",
"upload_time_iso_8601": "2025-10-24T00:17:01.697009Z",
"url": "https://files.pythonhosted.org/packages/23/d0/4744f8ffebfbbb9fac55fdcd82f172ee19dd8af70d13828062a63f364894/st_error_boundary-0.1.8-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "76883935e6bc245c89186a4798d3bc60b4f81a4ca4d4dcd0a24f38caf55adbf3",
"md5": "0fc694c4fd61a009f267f45fa1fc81fc",
"sha256": "b0aaa77af3521221b5cfe898cf82f765f30cc7f2729cb9cdf7ab4f0619fde5d1"
},
"downloads": -1,
"filename": "st_error_boundary-0.1.8.tar.gz",
"has_sig": false,
"md5_digest": "0fc694c4fd61a009f267f45fa1fc81fc",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.12",
"size": 95962,
"upload_time": "2025-10-24T00:17:02",
"upload_time_iso_8601": "2025-10-24T00:17:02.851225Z",
"url": "https://files.pythonhosted.org/packages/76/88/3935e6bc245c89186a4798d3bc60b4f81a4ca4d4dcd0a24f38caf55adbf3/st_error_boundary-0.1.8.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-10-24 00:17:02",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "K-dash",
"github_project": "st-error-boundary",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "st-error-boundary"
}