st-error-boundary


Namest-error-boundary JSON
Version 0.1.8 PyPI version JSON
download
home_pageNone
SummaryA tiny, typed error-boundary decorator for Streamlit apps (UI-safe fallback + pluggable hooks)
upload_time2025-10-24 00:17:02
maintainerNone
docs_urlNone
authorK-dash
requires_python>=3.12
licenseMIT
keywords decorator error-boundary error-handling streamlit
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![PyPI version](https://img.shields.io/pypi/v/st-error-boundary.svg)](https://pypi.org/project/st-error-boundary/)
[![Python versions](https://img.shields.io/pypi/pyversions/st-error-boundary.svg)](https://pypi.org/project/st-error-boundary/)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/st-error-boundary?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/st-error-boundary)
[![codecov](https://codecov.io/gh/K-dash/st-error-boundary/graph/badge.svg?token=nhDsSbTkaJ)](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": "[![PyPI version](https://img.shields.io/pypi/v/st-error-boundary.svg)](https://pypi.org/project/st-error-boundary/)\n[![Python versions](https://img.shields.io/pypi/pyversions/st-error-boundary.svg)](https://pypi.org/project/st-error-boundary/)\n[![PyPI Downloads](https://static.pepy.tech/personalized-badge/st-error-boundary?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/st-error-boundary)\n[![codecov](https://codecov.io/gh/K-dash/st-error-boundary/graph/badge.svg?token=nhDsSbTkaJ)](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"
}
        
Elapsed time: 1.04730s