Error Handling#

React errors can crash your entire UI. Use ErrorBoundary to catch errors and display fallback UI instead.

Import#

import { ErrorBoundary } from "onejs-react"

Basic Usage#

Wrap components that might throw errors:

<ErrorBoundary>
    <MyComponent />
</ErrorBoundary>

If MyComponent throws an error, ErrorBoundary displays a default error UI instead of crashing.

Custom Fallback#

Provide your own fallback UI with fallbackRender. It receives the error and a reset function that renders the children again:

<ErrorBoundary fallbackRender={({ error, reset }) => (
    <View>
        <Label text={`Something went wrong: ${error.message}`} />
        <Button text="Try Again" onClick={reset} />
    </View>
)}>
    <MyComponent />
</ErrorBoundary>

Fallback with Error Details#

errorInfo carries React's component stack. It is filled in on the render after the error, so it is null the first time the fallback runs:

<ErrorBoundary
    fallbackRender={({ error, errorInfo }) => (
        <View style={{ padding: 20, backgroundColor: "#2d1b1b" }}>
            <Label
                text={`Error: ${error.message}`}
                style={{ color: "#ff6666", fontSize: 16 }}
            />
            <Label
                text={errorInfo?.componentStack ?? ""}
                style={{ color: "#ffaaaa", fontSize: 12, marginTop: 10 }}
            />
        </View>
    )}
>
    <MyComponent />
</ErrorBoundary>

Props#

PropTypeDescription
childrenReactNodeComponents to wrap
fallbackRender({ error, errorInfo, reset }) => ReactNodeFallback UI
resetKeysunknown[]Reset when any entry changes
onError(error, errorInfo) => voidCalled once per caught error
onReset(details) => voidCalled after a reset; details.reason is "imperative-api" or "keys"

The older fallback prop, a node or an (error, errorInfo) function, still works but is deprecated.

Error Logging#

The runtime already logs every caught error to the Console once, with its component stack. Use onError to report it somewhere else:

<ErrorBoundary
    onError={(error, errorInfo) => {
        // Send to error tracking service
        trackError(error, errorInfo.componentStack)
    }}
>
    <App />
</ErrorBoundary>

Resetting the Boundary#

Calling reset from the fallback renders the children again. onReset runs before they do, which is where to clean up whatever caused the error:

<ErrorBoundary
    fallbackRender={({ reset }) => (
        <View>
            <Label text="Error occurred" />
            <Button text="Try Again" onClick={reset} />
        </View>
    )}
    onReset={() => resetAppState()}
>
    <MainContent />
</ErrorBoundary>

A boundary can also reset itself when what the failure depended on changes. Pass those values as resetKeys; when any of them changes while the fallback shows, the boundary tries the children again:

<ErrorBoundary resetKeys={[levelId]} fallbackRender={() => <Label text="This level failed to load" />}>
    <Level id={levelId} />
</ErrorBoundary>

Nested Boundaries#

Use multiple boundaries for different sections:

function App() {
    return (
        <View>
            <ErrorBoundary fallbackRender={() => <Label text="Header error" />}>
                <Header />
            </ErrorBoundary>

            <ErrorBoundary fallbackRender={() => <Label text="Content error" />}>
                <MainContent />
            </ErrorBoundary>

            <ErrorBoundary fallbackRender={() => <Label text="Footer error" />}>
                <Footer />
            </ErrorBoundary>
        </View>
    )
}

This way, an error in the header doesn't affect the rest of the app.

Format Error Utility#

The formatError helper creates readable error strings:

import { ErrorBoundary, formatError } from "onejs-react"

<ErrorBoundary
    fallbackRender={({ error, errorInfo }) => {
        const formatted = formatError(error, errorInfo?.componentStack ?? undefined)
        console.log(formatted)

        return <Label text="Error logged" />
    }}
>
    <MyComponent />
</ErrorBoundary>

Output format:

Error: Something went wrong

Stack:
  at MyComponent
  at View
  at App

Component:
  at MyComponent
  at ErrorBoundary
  at App

What Errors Are Caught#

ErrorBoundary catches:

  • Errors in render methods
  • Errors in lifecycle methods
  • Errors in constructors

ErrorBoundary does NOT catch:

  • Errors in event handlers (use try/catch)
  • Async errors (use try/catch in async functions)
  • Errors thrown outside React (global errors)

A promise that rejects with nothing to handle it is logged to the Console as an error, [OneJS] Unhandled promise rejection: followed by the reason and its stack, so a failed await with no catch is never silent. On WebGL the browser reports it in its own console instead.

Event Handler Errors#

Handle errors in event handlers manually:

function SafeButton() {
    const handleClick = () => {
        try {
            doSomethingRisky()
        } catch (error) {
            console.error("Click error:", error)
            // Show error to user
        }
    }

    return <Button text="Click" onClick={handleClick} />
}

Async Error Handling#

function AsyncComponent() {
    const [error, setError] = useState<string | null>(null)

    useEffect(() => {
        async function loadData() {
            try {
                const data = await fetchData()
                // Use data
            } catch (e) {
                setError(e instanceof Error ? e.message : String(e))
            }
        }
        loadData()
    }, [])

    if (error) {
        return <Label text={`Error: ${error}`} style={{ color: "#ff6666" }} />
    }

    return <Label text="Content" />
}

Full App Example#

import { render, ErrorBoundary, View, Label, Button } from "onejs-react"

function App() {
    return (
        <ErrorBoundary
            fallbackRender={({ error, reset }) => (
                <View style={{ padding: 40, alignItems: "center" }}>
                    <Label
                        text="Oops! Something went wrong."
                        style={{ fontSize: 20, color: "#ff6666", marginBottom: 20 }}
                    />
                    <Label
                        text={error.message}
                        style={{ color: "#ffaaaa", marginBottom: 20 }}
                    />
                    <Button text="Try Again" onClick={reset} />
                </View>
            )}
            onError={(error, info) => {
                // The runtime has already logged it; send it to a service here
                // analytics.trackError(error, info.componentStack)
            }}
        >
            <MainApp />
        </ErrorBoundary>
    )
}

render(<App />, __root)