Boundary

Boundary demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import Button from './common/Button';
import { Component } from '@expressive/react';

export default () => (
  <div className="container">
    <h1>Error Boundary</h1>
    <p>
      Override <code>catch()</code> and a Component becomes the boundary for
      everything it renders - per-feature error handling without nesting{' '}
      <code>&lt;ErrorBoundary&gt;</code> wrappers. Boundary below has no{' '}
      <code>render()</code> at all; it exists to sit in the tree and catch.
    </p>

    <Boundary>
      <div className="card">
        <h2>Handled in place</h2>
        <Recoverable />
      </div>

      <div className="card">
        <h2>Escalated</h2>
        <Escalating message="The widget gave up." />
      </div>
    </Boundary>

    <small>
      The first card keeps its error, so only it swaps to a fallback while the
      rest stays put. The second declines - its <code>catch()</code> rejects, so
      the boundary above takes the whole group.
    </small>
  </div>
);

class Boundary extends Component {
  error?: Error;
  resume = () => {};

  Fallback() {
    return (
      <div className="error">
        <p>Reached the boundary: {this.error?.message}</p>
        <Button primary onClick={() => this.resume()}>
          Start over
        </Button>
      </div>
    );
  }

  catch(error: Error) {
    this.error = error;
    this.fallback = <this.Fallback />;

    return new Promise<void>((resolve) => {
      this.resume = resolve;
    });
  }
}

class Fragile extends Component {
  message = 'The widget failed to render.';
  broken = false;

  Child() {
    if (this.broken) throw new Error(this.message);

    return <Button onClick={() => (this.broken = true)}>Break it</Button>;
  }

  render() {
    return <this.Child />;
  }
}

class Recoverable extends Fragile {
  resume = () => {};

  Fallback() {
    return (
      <div className="error">
        <p>Caught right here: {this.message}</p>
        <Button onClick={() => this.recover()}>Retry</Button>
      </div>
    );
  }

  // Returning a pending promise holds the fallback up; resolving immediately
  // would re-render, throw again, and loop.
  catch() {
    this.fallback = <this.Fallback />;

    return new Promise<void>((resolve) => {
      this.resume = resolve;
    });
  }

  recover() {
    this.broken = false;
    this.resume();
  }
}

class Escalating extends Fragile {
  async catch(error: Error) {
    this.broken = false;
    throw error;
  }
}

App.css

.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
}

.card h2 {
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin: 0;
}

.error {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-radius: var(--r-sm);
  border: 1px solid var(--accent);
  color: var(--fg);
  background: var(--accent-soft);
}

.error p {
  margin: 0;
}