mayu

Exceptions

Callback errors and render errors follow different paths. Callback errors are reported by the runtime, while render errors can be recovered by the nearest parent error boundary. Without a boundary, a render error reaches the document and is reported like a callback error.

Callback exception

Callback exceptions are logged and shown by the development error overlay. Error boundaries intentionally do not catch event-handler failures.

Count: 0

Show source
:ruby
  Button = import("/components/Form/Button")
  Card = import("/components/UI/Card")
  Details = import("/components/UI/Details")
  HighlightedCodeBlock = import("/components/UI/HighlightedCodeBlock")

  def initialize =
    @count = 0

  def handle_increment =
    @count += 1

  def handle_error =
    raise "The callback failed"

%Card
  .content
    %h3 Callback exception
    %p
      Callback exceptions are logged and shown by the development error
      overlay. Error boundaries intentionally do not catch event-handler
      failures.
    %p
      Count:
      %output<= @count
    .buttons
      %Button(onclick=handle_increment) Increment
      %Button(onclick=handle_error) Raise callback error
    %Details(summary="Show source")
      %HighlightedCodeBlock(language="haml")
        = File.read(File.join(__dir__, "CallbackException.haml"))

:css
  .content {
    padding: 1rem;
  }

  .buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 1em;
    margin: 1em 0;
  }

Render exception

The child below raises from its render method. This component catches the exception with handle_errorand renders a fallback.

The child is rendering normally.

Show boundary source
:ruby
  Button = import("/components/Form/Button")
  Card = import("/components/UI/Card")
  Details = import("/components/UI/Details")
  HighlightedCodeBlock = import("/components/UI/HighlightedCodeBlock")
  RenderException = import("./RenderException")

  def initialize
    @failed = false
    @error = nil
  end

  def handle_raise =
    @failed = true

  def handle_reset
    @failed = false
    @error = nil
  end

  def handle_error(error)
    @error = "#{error.class}: #{error.message}"
    true
  end

%Card
  .content
    %h3 Render exception
    = if @error
      .fallback(role="alert")
        %strong The error boundary recovered this subtree.
        %code= @error
        %Button(onclick=handle_reset) Reset example
    = else
      %p
        The child below raises from its render method. This component catches
        the exception with
        %code handle_error
        and renders a fallback.
      %Button(onclick=handle_raise) Raise child render error
      %RenderException(fail=@failed)
    %Details(summary="Show boundary source")
      %HighlightedCodeBlock(language="haml")
        = File.read(File.join(__dir__, "ErrorBoundaryExample.haml"))

:css
  .content,
  .fallback {
    display: grid;
    gap: 1rem;
  }

  .content {
    padding: 1rem;
  }

  .fallback {
    padding: 1rem;
    background: color-mix(in srgb, var(--accent-color), transparent 90%);
    border-left: 0.25rem solid var(--accent-color);
  }

Unhandled render exception

The child below raises from its render method, and nothing here catches it. The runtime reports the error in the server log with the path through the component tree, and the development overlay shows the same details. The page keeps its last good state until the next successful render, so Reset brings the child back.

The child is rendering normally.

Show source
:ruby
  Button = import("/components/Form/Button")
  Card = import("/components/UI/Card")
  Details = import("/components/UI/Details")
  HighlightedCodeBlock = import("/components/UI/HighlightedCodeBlock")
  RenderException = import("./RenderException")

  def initialize =
    @failed = false

  def handle_raise =
    @failed = true

  def handle_reset =
    @failed = false

%Card
  .content
    %h3 Unhandled render exception
    %p
      The child below raises from its render method, and nothing here catches
      it. The runtime reports the error in the server log with the path
      through the component tree, and the development overlay shows the same
      details. The page keeps its last good state until the next successful
      render, so Reset brings the child back.
    .buttons
      %Button(onclick=handle_raise) Raise unhandled render error
      %Button(onclick=handle_reset) Reset example
    %RenderException(fail=@failed)
    %Details(summary="Show source")
      %HighlightedCodeBlock(language="haml")
        = File.read(File.join(__dir__, "UnhandledRenderException.haml"))

:css
  .content {
    padding: 1rem;
  }

  .buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 1em;
    margin: 1em 0;
  }