A tiny, typed error-boundary decorator for Streamlit apps (UI-safe fallback + pluggable hooks)
Project description
st-error-boundary
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.
Features
- Minimal API: Just two required arguments (
on_errorandfallback) - Type-safe: Full Python 3.12+ type hints with strict mypy/pyright checking
- 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
pip install st-error-boundary
Quick Start
Basic Usage (Decorator Only)
For simple cases where you only need to protect the main function:
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()
Limitation: on_click/on_change callbacks are not protected with this approach.
Advanced Usage (With Callbacks)
To protect both decorated functions and widget callbacks:
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()
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:
- User clicks button with
on_click=callback - Streamlit executes
callback()-> Not protected by decorator - Streamlit reruns the script
- Decorated function executes -> Protected by decorator
Solution: Use boundary.wrap_callback() to explicitly wrap callbacks with the same error handling logic.
API Reference
ErrorBoundary
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 viast.error()) or a callable that renders custom UI
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
def hook(exc: Exception) -> None:
"""Handle exception with side effects."""
...
FallbackRenderer Protocol
def renderer(exc: Exception) -> None:
"""Render fallback UI for the exception."""
...
Examples
Multiple Hooks
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
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
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.
Workaround: Use the deferred rendering pattern to control error position. See Callback Rendering Position Guide for details.
Development
# Install dependencies
make install
# Run linting and type checking
make
# Run tests
make test
# Run example app
make example
License
MIT
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file st_error_boundary-0.1.0.tar.gz.
File metadata
- Download URL: st_error_boundary-0.1.0.tar.gz
- Upload date:
- Size: 69.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c54fa5d10dd2e6d73b9d441d1bd4bc8a6f96948a9b6450ea9fe1c65871417a90
|
|
| MD5 |
d8d7c684f2b036c72f8942e873e44904
|
|
| BLAKE2b-256 |
d0ddf696cd7f8c3c7ddfa46468ffbf9f32541e6aed5d85b4f378bf7b8ec6a0c2
|
Provenance
The following attestation bundles were made for st_error_boundary-0.1.0.tar.gz:
Publisher:
release.yml on K-dash/st-error-boundary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
st_error_boundary-0.1.0.tar.gz -
Subject digest:
c54fa5d10dd2e6d73b9d441d1bd4bc8a6f96948a9b6450ea9fe1c65871417a90 - Sigstore transparency entry: 584104842
- Sigstore integration time:
-
Permalink:
K-dash/st-error-boundary@6b00e445eee7ebe3027e369ab12097fd0367407c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/K-dash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6b00e445eee7ebe3027e369ab12097fd0367407c -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file st_error_boundary-0.1.0-py3-none-any.whl.
File metadata
- Download URL: st_error_boundary-0.1.0-py3-none-any.whl
- Upload date:
- Size: 6.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efcfedeb7be95c60351de6442772e59c80cfd8ee4a715960f579e118b5b7280c
|
|
| MD5 |
460f780d380d3e866be7f2209801c5e7
|
|
| BLAKE2b-256 |
4b5a5b9397a29235a4f8ab35c04d6818c2987222894348dd553a6bcb7e310a0c
|
Provenance
The following attestation bundles were made for st_error_boundary-0.1.0-py3-none-any.whl:
Publisher:
release.yml on K-dash/st-error-boundary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
st_error_boundary-0.1.0-py3-none-any.whl -
Subject digest:
efcfedeb7be95c60351de6442772e59c80cfd8ee4a715960f579e118b5b7280c - Sigstore transparency entry: 584104845
- Sigstore integration time:
-
Permalink:
K-dash/st-error-boundary@6b00e445eee7ebe3027e369ab12097fd0367407c -
Branch / Tag:
refs/heads/main - Owner: https://github.com/K-dash
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@6b00e445eee7ebe3027e369ab12097fd0367407c -
Trigger Event:
workflow_dispatch
-
Statement type: