Skip to main content
Solar surfaces two distinct categories of errors: ContractError from synchronous prop validation, and async errors from useResource. Both are designed to be actionable — ContractError carries a structured JSON payload an AI agent can parse and act on directly, and useResource errors land in your component’s render state rather than crashing the process. This guide covers both, and shows how to close the loop between a generation error and a corrected re-generation.

ContractError basics

A ContractError is thrown synchronously at the component call site the moment a prop contract is violated. It extends Error, so it behaves like any other thrown value — you catch it with try/catch.
import Button from './components/Button.js'

// label must be a string — passing a number throws immediately
Button({ label: 42, onClick: () => {} })

// ContractError: Button: prop "label": expected string, got number
The error message is human-readable, but the real value is the structured data on the error object itself.

ContractError JSON

Call err.toJSON() on any ContractError to get a plain object that is safe to serialize, log, and pass to an AI agent without further processing.
{
  "error": "ContractError",
  "component": "Button",
  "prop": "label",
  "expected": "string",
  "received": "number",
  "fix": "Pass a string value for \"label\"",
  "message": "Button: prop \"label\": expected string, got number"
}
The fix field is written in plain English and targets the exact issue. An agent can read it directly without requiring additional context about the component or its schema.

Catching ContractErrors

Wrap any component call where props come from dynamic or agent-generated input. Check err.name === 'ContractError' to distinguish prop validation failures from unexpected runtime errors, then handle them separately.
import { mountComponent, ContractError } from './solar/index.js'
import Button from './components/Button.js'

try {
  const container = document.getElementById('app')
  mountComponent(Button, { label: userInput, onClick: handler }, container)
} catch (err) {
  if (err.name === 'ContractError') {
    // Safe to serialize — toJSON() returns a plain object
    logError(err.toJSON())

    // Or display it in your UI for debugging
    document.getElementById('error-output').textContent =
      JSON.stringify(err.toJSON(), null, 2)
  } else {
    // Not a contract violation — re-throw so other error handlers can catch it
    throw err
  }
}
You can also check err instanceof ContractError if you import the class. Use whichever form fits your codebase — both are equivalent. The name check works without importing the class, which is useful in generated code that may not import every Solar export.

ContractError fields

Every ContractError carries these fields on the instance and in toJSON():
FieldTypeDescription
errorstringAlways "ContractError". Use this to identify the error type when deserializing JSON payloads.
componentstringThe name of the component that threw — for example, "Button".
propstring | nullThe prop that failed validation. null for component-level errors (e.g. registering a non-component).
expectedstringWhat Solar expected — for example, "string" or "slot(Button)".
receivedstringWhat was actually passed — for example, "number" or "vnode with no _source".
fixstringA plain-English instruction for correcting the error. Safe to pass to an AI agent as-is.
messagestringThe full error message combining component, prop, expected, and received.

Async errors from useResource

Errors from useResource do not throw — they land in the error field of the hook’s return value and trigger a re-render. Check error in your render function and return a fallback UI.
import { createElement, useResource } from './solar/index.js'

// Inside a component's render function:
const { data, loading, error } = useResource({
  key: id,
  fetch: async (signal) => {
    const res = await fetch(`/api/records/${id}`, { signal })
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res.json()
  },
})

if (loading) return createElement('p', {}, 'Loading...')
if (error)   return createElement('p', {}, `Failed to load: ${error.message}`)

return createElement('div', {},
  createElement('h3', {}, data.title),
)
The error is the raw Error object your fetch function threw — so error.message contains whatever message you set. Throw descriptive errors from your fetch function (new Error('HTTP 404'), new Error('Unauthorized')) so the message is useful in the UI and in logs.
If an in-flight request is aborted (because the key changed), the abort rejection is silently swallowed by useResource. You will not see an error state from a cancellation — only from a genuine fetch failure. Design your error UI for real failures, not expected cancellations.

Using errors for AI self-correction

ContractError’s structured toJSON() output is designed for this loop: generate code → run it → catch the error → feed it back → regenerate.
async function generateAndMount(agent, prompt, container) {
  // Step 1: Agent generates component code
  let code = await agent.generate(prompt)

  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      // Step 2: Dynamically load and run the generated code
      const { default: Component } = await loadComponentCode(code)

      // Step 3: Mount — this validates props at the call site
      mountComponent(Component, testProps, container)
      return // Success — exit the loop
    } catch (err) {
      if (err.name !== 'ContractError') throw err

      // Step 4: Serialize the error and send it back to the agent
      code = await agent.send(
        `Your component threw a ContractError. Fix the issue described below ` +
        `and regenerate the full component file:\n\n` +
        JSON.stringify(err.toJSON(), null, 2)
      )
      // Step 5: Loop — try mounting the corrected code
    }
  }

  throw new Error('Component failed to validate after 3 attempts')
}
The fix field in the error payload gives the agent a precise, actionable instruction. In practice, a single correction round resolves most prop type errors because the expected and received fields fully describe the mismatch.

Registry validation

registry.register() also throws a ContractError if you pass it a value that was not created with defineComponent(). This catches the common mistake of passing a plain function or object instead of a component.
import { registry } from './solar/index.js'

function PlainButton(props) {
  return createElement('button', {}, props.label)
}

try {
  registry.register(PlainButton) // Throws
} catch (err) {
  if (err.name === 'ContractError') {
    // fix: "Wrap this function with defineComponent() before registering"
    console.log(err.fix)
  }
}
Always call defineComponent() before registry.register(). The _isComponent flag on the returned function is what registry.register() checks.
Solar warns for unknown props rather than throwing. If you pass a prop that is not declared in the component’s schema, Solar calls console.warn — for example, "Button: unknown prop \"size\" passed but not declared in schema" — and continues rendering with the declared props. This keeps unknown-prop mistakes visible in the console without breaking the component.