Domphy

Error Handling

Catching errors in render functions

Domphy render functions (listener callbacks) run synchronously. Wrap them in try/catch if they can throw:

import { toState } from "@domphy/core"

const userData = toState<unknown>(null)

const UserCard = {
  div: (l) => {
    try {
      const user = userData.get(l) as { name: string }
      return `Hello, ${user.name}`
    } catch {
      return "Unable to display user"
    }
  },
}

Error boundary patch

@domphy/ui provides an errorBoundary patch that catches render errors in its subtree:

import { errorBoundary } from "@domphy/ui"

const SafeWidget = {
  div: RiskyWidget,
  $: [errorBoundary({
    fallback: (error) => ({
      div: `Something went wrong: ${error.message}`,
      style: { color: "red" },
    }),
    onError: (error, info) => console.error("Widget error:", error, info),
  })],
}

errorBoundary installs a try/catch around the subtree's render and re-mount cycle. When a child throws, the fallback function renders instead.

Async error handling

For async operations (fetching, saving), keep error state alongside loading state:

import { toState } from "@domphy/core"

interface AsyncState<T> {
  data: T | null
  loading: boolean
  error: string | null
}

const post = toState<AsyncState<Post>>({ data: null, loading: false, error: null })

async function loadPost(id: string) {
  post.set((s) => ({ ...s, loading: true, error: null }))
  try {
    const data = await fetchPost(id)
    post.set({ data, loading: false, error: null })
  } catch (err) {
    post.set((s) => ({ ...s, loading: false, error: (err as Error).message }))
  }
}

const PostView = {
  div: (l) => {
    const { data, loading, error } = post.get(l)
    if (loading) return { div: "Loading…" }
    if (error)   return { div: error, style: { color: "red" } }
    if (!data)   return { div: "No post" }
    return { article: [{ h1: data.title }, { p: data.body }] }
  },
}

Global error handler

Catch unhandled promise rejections and runtime errors globally:

window.addEventListener("unhandledrejection", (event) => {
  console.error("Unhandled promise rejection:", event.reason)
  showErrorToast(String(event.reason))
})

window.addEventListener("error", (event) => {
  console.error("Global error:", event.error)
  showErrorToast(event.message)
})

Error recovery

Allow users to retry after an error with a reset mechanism:

import { toState } from "@domphy/core"
import { button } from "@domphy/ui"

const fetchState = toState<"idle" | "loading" | "error">("idle")
const errorMessage = toState("")

async function load() {
  fetchState.set("loading")
  try {
    await fetchData()
    fetchState.set("idle")
  } catch (err) {
    errorMessage.set((err as Error).message)
    fetchState.set("error")
  }
}

const Widget = {
  div: (l) => {
    switch (fetchState.get(l)) {
      case "loading": return { div: "Loading…" }
      case "error":   return {
        div: [
          { p: (l) => errorMessage.get(l) },
          {
            button: "Retry",
            $: [button()],
            onClick: () => load(),
          },
        ],
      }
      default: return { div: "Content loaded" }
    }
  },
}

Error boundaries with @domphy/query

When using @domphy/query, set throwOnError: true to let query errors propagate to the nearest error boundary:

import { QueryClient } from "@domphy/query"
import { createQuery } from "@domphy/query/domphy"
import { errorBoundary } from "@domphy/ui"

const queryClient = new QueryClient()

const query = createQuery(queryClient, {
  queryKey: () => ["post", id],
  queryFn: () => fetchPost(id),
  throwOnError: true,   // error propagates to errorBoundary
})

const SafePost = {
  div: PostContent,
  $: [errorBoundary({
    fallback: (error) => ({
      div: `Failed to load post: ${(error as Error).message}`,
    }),
  })],
}

Logging errors

Structured error logging with context:

function logError(error: Error, context: Record<string, unknown> = {}) {
  console.error({
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString(),
    ...context,
  })
  // send to your error tracking service
  errorTracker?.captureException(error, { extra: context })
}

Use in async handlers:

async function saveForm(data: FormData) {
  try {
    await api.post("/form", data)
  } catch (err) {
    logError(err as Error, { action: "saveForm", formId: data.id })
    throw err   // re-throw so the caller can update UI
  }
}