# Error boundary

> Draws a part of the view, or an error card in its place when it fails. A Pyzed component for Python desktop GUIs, with examples and its API.

Source: https://albakore.github.io/pyzed/library/error-boundary/

:::tip[In plain words]
A safety net for one part of a window: if that part breaks, a small error box shows there and the rest of the app keeps working.
:::

`ui.error_boundary(build)` returns `build()` or, when it raises, an error card in its place, so one broken part of the view (a sidebar, a panel) never takes the whole window down. The card shows the error, the file and line it comes from and a Copy error button; `on_details` adds a Details button and `fallback` replaces the card with your own widget. `on_error(info)` is told about the error each time the part fails to draw. `ui.ErrorInfo` describes an error (its type, message, traceback and origin) and `ui.error_card` is the card on its own.

`ui.error_boundary` · Category: [Feedback](https://albakore.github.io/pyzed/library/#feedback) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_boundary)

**Code:**

```python
def totals():
    values = []
    return ui.label(f"Average: {sum(values) / len(values)}")

pz.row([
    ui.card(ui.muted("This panel keeps working."), title="Files", width=200),
    pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details), width=380),
], spacing=12, align="center")
```

:::note[New]
This component was added recently. Tell us how it works for you in the [issue tracker](https://github.com/albakore/pyzed/issues).
:::

## When to use

- Around each panel of a big window, so a bug in one leaves the others working.
- Around parts that draw data you don't control: a plugin, a file the user opened, a reply from a server.
- Open an [Error dialog](https://albakore.github.io/pyzed/library/error-dialog/) from `on_details` to show the whole traceback.

## Usage

```python
import pyzed as pz
from pyzed import ui

class Report(pz.App):
    def __init__(self):
        self.error = None  # the ui.ErrorInfo shown in the dialog

    def totals(self):
        ...  # may raise

    def show(self, info):
        self.error = info

    def close(self):
        self.error = None

    def view(self):
        page = pz.row([
            self.files(),
            ui.error_boundary(self.totals, title="Totals", on_details=self.show),
        ])
        dialog = ui.error_dialog(self.error, self.close) if self.error else None
        return ui.dialog_layer(page, dialog, on_dismiss=self.close)
```

## Variants

:::note[About these captures]
Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks.
:::

### Boundary

While `build()` works the boundary returns its widget untouched; only a part that raises is replaced.

```python
ui.error_boundary(lambda: ui.label("Average: 4.2"))
```

```python
def totals():
    values = []
    return ui.label(f"Average: {sum(values) / len(values)}")

pz.container(ui.error_boundary(totals), width=360)
```

```python
def totals():
    values = []
    return ui.label(f"Average: {sum(values) / len(values)}")

pz.container(ui.error_boundary(totals, title="Totals"), width=360)
```

```python
def totals():
    values = []
    return ui.label(f"Average: {sum(values) / len(values)}")

pz.container(ui.error_boundary(totals, title="Totals", on_details=on_details),
             width=360)
```

```python
def totals():
    values = []
    return ui.label(f"Average: {sum(values) / len(values)}")

ui.error_boundary(totals, fallback=lambda info: ui.callout(
    "Totals are not available", info.summary, tint="warning"))
```

### Error card

`ui.error_card` is the card on its own, for errors you catch yourself.

```python
info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form")
pz.container(ui.error_card(info), width=360)
```

```python
info = ui.ErrorInfo("KeyError", "'email'", origin=("app/forms.py", 42), where="Sign-up form")
pz.container(ui.error_card(info, title="The form could not be loaded",
                           actions=[ui.button("Retry", on_retry, icon="rotate-ccw",
                                              size="sm", variant="ghost")]),
             width=360)
```

```python
pz.container(ui.error_card(ui.ErrorInfo("TimeoutError", "the server took too long")),
             width=360)
```

## Events

Error boundary has **2 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/).

| Event | Of | Handler gets | When |
| --- | --- | --- | --- |
| `on_error` | `ui.error_boundary` | `(info)` | Receives the `ui.ErrorInfo`. |
| `on_details` | `ui.error_boundary`, `ui.error_card` | `(info)` | Adds a Details button; receives the `ui.ErrorInfo` (open a `ui.error_dialog`). |

**Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`.

### Handling the events

The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/).

**Imperative:**

```python
class Report(pz.App):
    def __init__(self):
        self.values = []
        self.error = None                # the ui.ErrorInfo shown in the dialog

    def totals(self):
        return ui.label(f"Average: {sum(self.values) / len(self.values)}")   # raises on []

    def view(self):
        page = ui.error_boundary(self.totals, title="Totals",
                                 on_error=lambda info: print(info.message),         # every failure
                                 on_details=lambda info: setattr(self, "error", info))  # Details
        dialog = ui.error_dialog(self.error, lambda: setattr(self, "error", None)) if self.error else None
        return ui.dialog_layer(page, dialog)
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class ShowError:
    info: object

class Report(pz.App):
    error = None

    def totals(self):
        return ui.label(f"Average: {sum([]) / len([])}")

    def update(self, message):
        match message:
            case ShowError(info):
                self.error = info
            case "close":
                self.error = None

    def view(self):
        page = ui.error_boundary(self.totals, title="Totals", on_details=ShowError)
        return ui.dialog_layer(page, ui.error_dialog(self.error, "close") if self.error else None)
```

**Functional:**

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class State:
    values: tuple = ()
    failures: int = 0

def totals(values):
    return ui.label(f"Average: {sum(values) / len(values)}")

def view(state, set_state):
    return pz.column([
        ui.error_boundary(lambda: totals(state.values), title="Totals",
                          on_error=lambda info: set_state(failures=state.failures + 1)),
        ui.button("Add 10", lambda: set_state(values=state.values + (10,))),
    ], spacing=8, width=380)
```

## Tips

:::tip
The boundary runs `build()` on every redraw, so `on_error` is called again each time the part fails. To report an error once, compare it with the last one: two `ErrorInfo` with the same type, message, traceback and origin are equal (`time` is left out). `ErrorInfo.from_exception(err)` describes any exception you catch yourself, and `info.report()` gives the text to paste in a bug report.
:::

## Properties and API

### `ui.error_boundary`

```python
error_boundary(
    build,
    *,
    on_error=None,
    title=None,
    where='',
    on_details=None,
    fallback=None,
    theme=None,
)
```

Draws `build()`, or an error card when it raises, so one part of the view that fails never takes the whole window down (like React's error boundaries). `on_error(info)` is told about the error each time the part is drawn and fails, so report it once (compare `info.traceback`).

| Property | Default | Description |
| --- | --- | --- |
| `build` | *required* | A function that returns the part's widget. |
| `title` | `None` | Heading of the card (and the error's `where`). |
| `where` | `""` | What the part is, for the report. |
| `fallback` | `None` | Your own widget for the error: `fallback(info)`. |
| `theme` | `None` | Theme override. |

Events: `on_error`, `on_details` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_boundary)

### `ui.error_card`

```python
error_card(info, *, title=None, on_details=None, actions=(), theme=None)
```

The card `ui.error_boundary` draws in place of what failed: the error, where it comes from and a Copy error button (and Details with `on_details`).

| Property | Default | Description |
| --- | --- | --- |
| `info` | *required* | A `ui.ErrorInfo`. |
| `title` | `None` | Heading. |
| `actions` | `()` | More buttons after them. |
| `theme` | `None` | Theme override. |

Events: `on_details` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#error_card)

### `ui.ErrorInfo`

```python
@dataclass
class ErrorInfo(kind, message, traceback='', origin=None, where='', time=0.0)
```

An error as `ui.error_card` and `ui.error_dialog` show it: `kind` (the exception's type), `message`, the whole `traceback`, the `origin` `(path, line)` it comes from (the last frame outside the standard library, installed packages and Pyzed), `where` it happened and `time`. `ErrorInfo.from_exception(err, where=...)` makes one (`prefer=` picks which frames may be the origin); `ErrorInfo.current()` describes the exception being handled. `summary` is `Type: message`, `location` is `file.py:12` and `report()` is everything as text for a bug report.

| Property | Default | Description |
| --- | --- | --- |
| `kind` | *required* | The exception's type name. |
| `message` | *required* | Its message. |
| `traceback` | `""` | The whole traceback, as Python prints it. |
| `origin` | `None` | `(path, line)` of the code it comes from, or `None`. |
| `where` | `""` | What was running. |
| `time` | `0.0` | When it happened (`time.time()`). |

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#errorinfo)

## Related

- [Error dialog](https://albakore.github.io/pyzed/library/error-dialog/): What went wrong, where, and the traceback, with Copy and Go to source.
- [Callout](https://albakore.github.io/pyzed/library/callout/): An inline notice with a title, text and actions.
- [Console](https://albakore.github.io/pyzed/library/console/): A dev-tools style log with levels, filter, copy and clear.
