diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 25c7a3d66f0..52785bb452e 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -192,6 +192,7 @@ const config = {
'ButtonProps',
'Dialog',
'DialogProps',
+ 'ErrorBoundary',
'MenuButton',
'MenuButtonProps',
'MenuGroup',
diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts
index 43eefde3dc4..f2b4bf85270 100644
--- a/dev/test-studio/sanity.config.ts
+++ b/dev/test-studio/sanity.config.ts
@@ -153,6 +153,13 @@ const defaultWorkspace = {
projectId: 'ppsg7ml5',
dataset: 'test',
plugins: [sharedSettings()],
+
+ onUncaughtError: (error, errorInfo) => {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ // eslint-disable-next-line no-console
+ console.log(errorInfo)
+ },
basePath: '/test',
icon: SanityMonogram,
// eslint-disable-next-line camelcase
@@ -246,6 +253,12 @@ export default defineConfig([
dataset: 'test',
plugins: [sharedSettings(), studioComponentsPlugin(), formComponentsPlugin()],
basePath: '/custom-components',
+ onUncaughtError: (error, errorInfo) => {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ // eslint-disable-next-line no-console
+ console.log(errorInfo)
+ },
form: {
components: {
input: Input,
diff --git a/packages/sanity/src/core/config/configPropertyReducers.ts b/packages/sanity/src/core/config/configPropertyReducers.ts
index 0c93ce5cc1c..c2b6a66205a 100644
--- a/packages/sanity/src/core/config/configPropertyReducers.ts
+++ b/packages/sanity/src/core/config/configPropertyReducers.ts
@@ -1,5 +1,5 @@
import {type AssetSource, type SchemaTypeDefinition} from '@sanity/types'
-import {type ReactNode} from 'react'
+import {type ErrorInfo, type ReactNode} from 'react'
import {type LocaleConfigContext, type LocaleDefinition, type LocaleResourceBundle} from '../i18n'
import {type Template, type TemplateItem} from '../templates'
@@ -310,6 +310,29 @@ export const documentCommentsEnabledReducer = (opts: {
return result
}
+export const onUncaughtErrorResolver = (opts: {
+ config: PluginOptions
+ context: {error: Error; errorInfo: ErrorInfo}
+}) => {
+ const {config, context} = opts
+ const flattenedConfig = flattenConfig(config, [])
+ flattenedConfig.forEach(({config: pluginConfig}) => {
+ // There is no concept of 'previous value' in this API. We only care about the final value.
+ // That is, if a plugin returns true, but the next plugin returns false, the result will be false.
+ // The last plugin 'wins'.
+ const resolver = pluginConfig.onUncaughtError
+
+ if (typeof resolver === 'function') return resolver(context.error, context.errorInfo)
+ if (!resolver) return undefined
+
+ throw new Error(
+ `Expected \`document.onUncaughtError\` to be a a function, but received ${getPrintableType(
+ resolver,
+ )}`,
+ )
+ })
+}
+
export const internalTasksReducer = (opts: {
config: PluginOptions
}): {footerAction: ReactNode} | undefined => {
diff --git a/packages/sanity/src/core/config/prepareConfig.ts b/packages/sanity/src/core/config/prepareConfig.ts
index f90070cbbf6..054250acc5b 100644
--- a/packages/sanity/src/core/config/prepareConfig.ts
+++ b/packages/sanity/src/core/config/prepareConfig.ts
@@ -4,7 +4,13 @@ import {type CurrentUser, type Schema, type SchemaValidationProblem} from '@sani
import {studioTheme} from '@sanity/ui'
import {type i18n} from 'i18next'
import {startCase} from 'lodash'
-import {type ComponentType, createElement, type ElementType, isValidElement} from 'react'
+import {
+ type ComponentType,
+ createElement,
+ type ElementType,
+ type ErrorInfo,
+ isValidElement,
+} from 'react'
import {isValidElementType} from 'react-is'
import {map, shareReplay} from 'rxjs/operators'
@@ -32,6 +38,7 @@ import {
internalTasksReducer,
legacySearchEnabledReducer,
newDocumentOptionsResolver,
+ onUncaughtErrorResolver,
partialIndexingEnabledReducer,
resolveProductionUrlReducer,
schemaTemplatesReducer,
@@ -626,6 +633,16 @@ function resolveSource({
staticInitialValueTemplateItems,
options: config,
},
+ onUncaughtError: (error: Error, errorInfo: ErrorInfo) => {
+ return onUncaughtErrorResolver({
+ config,
+ context: {
+ error: error,
+ errorInfo: errorInfo,
+ },
+ })
+ },
+
beta: {
treeArrayEditing: {
// This beta feature is no longer available.
diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts
index 6f223b42e60..f8ef6d8ffdf 100644
--- a/packages/sanity/src/core/config/types.ts
+++ b/packages/sanity/src/core/config/types.ts
@@ -10,7 +10,7 @@ import {
type SchemaTypeDefinition,
} from '@sanity/types'
import {type i18n} from 'i18next'
-import {type ComponentType, type ReactNode} from 'react'
+import {type ComponentType, type ErrorInfo, type ReactNode} from 'react'
import {type Observable} from 'rxjs'
import {type Router, type RouterState} from 'sanity/router'
@@ -392,6 +392,10 @@ export interface PluginOptions {
* @internal
*/
beta?: BetaFeatures
+ /** Configuration for error handling.
+ * @beta
+ */
+ onUncaughtError?: (error: Error, errorInfo: ErrorInfo) => void
}
/** @internal */
@@ -781,6 +785,10 @@ export interface Source {
* @internal
*/
beta?: BetaFeatures
+ /** Configuration for error handling.
+ * @internal
+ */
+ onUncaughtError?: (error: Error, errorInfo: ErrorInfo) => void
}
/** @internal */
diff --git a/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.test.tsx b/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.test.tsx
new file mode 100644
index 00000000000..297b489ac0e
--- /dev/null
+++ b/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.test.tsx
@@ -0,0 +1,57 @@
+import {beforeAll, describe, expect, it, jest} from '@jest/globals'
+import {render, screen} from '@testing-library/react'
+import {type SanityClient} from 'sanity'
+
+import {createMockSanityClient} from '../../../../test/mocks/mockSanityClient'
+import {createTestProvider} from '../../../../test/testUtils/TestProvider'
+import {FormBuilderInputErrorBoundary} from './FormBuilderInputErrorBoundary'
+
+jest.mock('use-hot-module-reload', () => ({
+ useHotModuleReload: jest.fn(),
+}))
+
+describe('FormBuilderInputErrorBoundary', () => {
+ beforeAll(() => {
+ jest.clearAllMocks()
+ })
+
+ it('renders children when there is no error', async () => {
+ render(
+
+ Child Component
+ ,
+ )
+
+ expect(screen.getByTestId('child')).toBeInTheDocument()
+ })
+
+ it('calls onUncaughtError when an error is caught', async () => {
+ const onUncaughtError = jest.fn()
+
+ const ThrowErrorComponent = () => {
+ throw new Error('An EXPECTED, testing error occurred!')
+ }
+
+ const client = createMockSanityClient() as unknown as SanityClient
+
+ const TestProvider = await createTestProvider({
+ client,
+ config: {
+ name: 'default',
+ projectId: 'test',
+ dataset: 'test',
+ onUncaughtError,
+ },
+ })
+
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(onUncaughtError).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.tsx b/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.tsx
index 8bf4b795a90..17caafefdb0 100644
--- a/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.tsx
+++ b/packages/sanity/src/core/form/studio/FormBuilderInputErrorBoundary.tsx
@@ -1,7 +1,8 @@
-import {Box, Card, Code, ErrorBoundary, Stack, Text} from '@sanity/ui'
+import {Box, Card, Code, Stack, Text} from '@sanity/ui'
import {useCallback, useMemo, useState} from 'react'
import {useHotModuleReload} from 'use-hot-module-reload'
+import {ErrorBoundary} from '../../../ui-components/errorBoundary'
import {SchemaError} from '../../config'
import {isDev} from '../../environment'
import {useTranslation} from '../../i18n'
diff --git a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
index 32b0de92a00..d1e87257fc3 100644
--- a/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
+++ b/packages/sanity/src/core/studio/StudioErrorBoundary.tsx
@@ -1,16 +1,6 @@
/* eslint-disable i18next/no-literal-string */
/* eslint-disable @sanity/i18n/no-attribute-string-literals */
-import {
- Box,
- Card,
- Code,
- Container,
- ErrorBoundary,
- type ErrorBoundaryProps,
- Heading,
- Stack,
- Text,
-} from '@sanity/ui'
+import {Box, Card, Code, Container, type ErrorBoundaryProps, Heading, Stack, Text} from '@sanity/ui'
import {
type ComponentType,
type ErrorInfo,
@@ -23,6 +13,7 @@ import {ErrorActions, isDev, isProd} from 'sanity'
import {styled} from 'styled-components'
import {useHotModuleReload} from 'use-hot-module-reload'
+import {ErrorBoundary} from '../../ui-components'
import {SchemaError} from '../config'
import {errorReporter} from '../error/errorReporter'
import {CorsOriginError} from '../store'
diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/FilterForm.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/FilterForm.tsx
index 237fc7d500f..005603a0e90 100644
--- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/FilterForm.tsx
+++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/FilterForm.tsx
@@ -1,9 +1,9 @@
import {TrashIcon} from '@sanity/icons'
-import {Box, Card, ErrorBoundary, Flex, Stack, Text} from '@sanity/ui'
+import {Box, Card, Flex, Stack, Text} from '@sanity/ui'
import {type ErrorInfo, useCallback, useState} from 'react'
import FocusLock from 'react-focus-lock'
-import {Button} from '../../../../../../../../ui-components'
+import {Button, ErrorBoundary} from '../../../../../../../../ui-components'
import {supportsTouch} from '../../../../../../../util'
import {useSearchState} from '../../../contexts/search/useSearchState'
import {getFilterDefinition} from '../../../definitions/filters'
diff --git a/packages/sanity/src/core/studio/workspaceLoader/WorkspaceLoader.tsx b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceLoader.tsx
index b6186b1368f..3899eba68a3 100644
--- a/packages/sanity/src/core/studio/workspaceLoader/WorkspaceLoader.tsx
+++ b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceLoader.tsx
@@ -1,8 +1,8 @@
-import {ErrorBoundary} from '@sanity/ui'
import {type ComponentType, type ReactNode, useEffect, useState} from 'react'
import {combineLatest, of} from 'rxjs'
import {catchError, map} from 'rxjs/operators'
+import {ErrorBoundary} from '../../../ui-components'
import {
ConfigResolutionError,
type Source,
diff --git a/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.test.tsx b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.test.tsx
new file mode 100644
index 00000000000..14044f080e0
--- /dev/null
+++ b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.test.tsx
@@ -0,0 +1,104 @@
+import {describe, expect, it, jest} from '@jest/globals'
+import {render, screen} from '@testing-library/react'
+import {type SanityClient, type Workspace} from 'sanity'
+
+import {createMockSanityClient} from '../../../../test/mocks/mockSanityClient'
+import {createTestProvider} from '../../../../test/testUtils/TestProvider'
+import {WorkspaceRouterProvider} from './WorkspaceRouterProvider'
+
+jest.mock('../router/RouterHistoryContext', () => ({
+ useRouterHistory: () => ({
+ location: {pathname: '/'},
+ listen: jest.fn(),
+ }),
+}))
+
+jest.mock('../router', () => ({
+ createRouter: () => ({
+ getBasePath: jest.fn(),
+ decode: jest.fn(),
+ isNotFound: jest.fn(),
+ }),
+}))
+
+jest.mock('sanity/router', () => ({
+ RouterProvider: ({children}: {children: React.ReactNode}) =>
{children}
,
+ IntentLink: () => IntentLink
,
+}))
+
+jest.mock('./WorkspaceRouterProvider', () => ({
+ ...(jest.requireActual('./WorkspaceRouterProvider') as object),
+ useRouterFromWorkspaceHistory: jest.fn(),
+}))
+
+describe('WorkspaceRouterProvider', () => {
+ const LoadingComponent = () => Loading...
+ const children = Children
+ const workspace = {
+ basePath: '',
+ tools: [],
+ icon: null,
+ unstable_sources: [],
+ scheduledPublishing: false,
+ document: {},
+ form: {},
+ search: {},
+ title: 'Default Workspace',
+ name: 'default',
+ projectId: 'test',
+ dataset: 'test',
+ schema: {},
+ templates: {},
+ currentUser: {},
+ authenticated: true,
+ auth: {},
+ getClient: jest.fn(),
+ i18n: {},
+ __internal: {},
+ type: 'workspace',
+ // Add other required properties with appropriate default values
+ } as unknown as Workspace
+
+ it('renders children when state is not null', () => {
+ render(
+
+ {children}
+ ,
+ )
+
+ expect(screen.getByText('Children')).toBeInTheDocument()
+ })
+
+ it('calls onUncaughtError when an error is caught', async () => {
+ const onUncaughtError = jest.fn()
+
+ const ThrowErrorComponent = () => {
+ throw new Error('An EXPECTED, testing error occurred!')
+ }
+
+ const client = createMockSanityClient() as unknown as SanityClient
+
+ const TestProvider = await createTestProvider({
+ client,
+ config: {
+ name: 'default',
+ projectId: 'test',
+ dataset: 'test',
+ onUncaughtError,
+ },
+ })
+
+ try {
+ render(
+
+ {/* prevents thrown error from breaking the test */}
+
+
+
+ ,
+ )
+ } catch {
+ expect(onUncaughtError).toHaveBeenCalledTimes(1)
+ }
+ })
+})
diff --git a/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.tsx b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.tsx
index eeaeb2f99c2..7032a02d867 100644
--- a/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.tsx
+++ b/packages/sanity/src/core/studio/workspaceLoader/WorkspaceRouterProvider.tsx
@@ -3,6 +3,7 @@ import {
type ComponentType,
type MutableRefObject,
type ReactNode,
+ useCallback,
useEffect,
useMemo,
useRef,
@@ -10,6 +11,7 @@ import {
import {type Router, RouterProvider, type RouterState} from 'sanity/router'
import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector.js'
+import {ErrorBoundary} from '../../../ui-components'
import {type Tool, type Workspace} from '../../config'
import {createRouter, type RouterHistory, type RouterStateEvent} from '../router'
import {decodeUrlState, resolveDefaultState, resolveIntentState} from '../router/helpers'
@@ -31,14 +33,21 @@ export function WorkspaceRouterProvider({
const router = useMemo(() => createRouter({basePath, tools}), [basePath, tools])
const [state, onNavigate] = useRouterFromWorkspaceHistory(history, router, tools)
+ const handleCatchError = useCallback(({error}: {error: Error}) => {
+ /** catches errors in studio that bubble up, throwing the error */
+ throw error
+ }, [])
+
// `state` is only null if the Studio is somehow rendering in SSR or using hydrateRoot in combination with `unstable_noAuthBoundary`.
// Which makes this loading condition extremely rare, most of the time it'll render `RouteProvider` right away.
if (!state) return
return (
-
- {children}
-
+
+
+ {children}
+
+
)
}
diff --git a/packages/sanity/src/structure/components/confirmDeleteDialog/index.tsx b/packages/sanity/src/structure/components/confirmDeleteDialog/index.tsx
index 9d4a76510aa..ddc95ef0ee1 100644
--- a/packages/sanity/src/structure/components/confirmDeleteDialog/index.tsx
+++ b/packages/sanity/src/structure/components/confirmDeleteDialog/index.tsx
@@ -1,8 +1,8 @@
-import {Box, ErrorBoundary, Text} from '@sanity/ui'
+import {Box, Text} from '@sanity/ui'
import {type ComponentProps, useCallback, useId, useState} from 'react'
import {useTranslation} from 'sanity'
-import {Dialog} from '../../../ui-components'
+import {Dialog, ErrorBoundary} from '../../../ui-components'
import {structureLocaleNamespace} from '../../i18n'
import {ConfirmDeleteDialog, type ConfirmDeleteDialogProps} from './ConfirmDeleteDialog'
diff --git a/packages/sanity/src/structure/components/structureTool/StructureToolBoundary.tsx b/packages/sanity/src/structure/components/structureTool/StructureToolBoundary.tsx
index a9fbb7a3063..6eeff905e35 100644
--- a/packages/sanity/src/structure/components/structureTool/StructureToolBoundary.tsx
+++ b/packages/sanity/src/structure/components/structureTool/StructureToolBoundary.tsx
@@ -1,7 +1,7 @@
-import {ErrorBoundary} from '@sanity/ui'
import {useEffect, useState} from 'react'
import {SourceProvider, type Tool, useWorkspace} from 'sanity'
+import {ErrorBoundary} from '../../../ui-components/errorBoundary'
import {setActivePanes} from '../../getIntentState'
import {StructureToolProvider} from '../../StructureToolProvider'
import {type StructureToolOptions} from '../../types'
@@ -25,6 +25,7 @@ export function StructureToolBoundary({tool: {options}}: StructureToolBoundaryPr
}, [])
const [{error}, setError] = useState<{error: unknown}>({error: null})
+
// this re-throws if the error it catches is not a PaneResolutionError
if (error) return
diff --git a/packages/sanity/src/structure/panes/document/inspectors/validation/ValidationInspector.tsx b/packages/sanity/src/structure/panes/document/inspectors/validation/ValidationInspector.tsx
index b3d7c94e582..535d74b3e62 100644
--- a/packages/sanity/src/structure/panes/document/inspectors/validation/ValidationInspector.tsx
+++ b/packages/sanity/src/structure/panes/document/inspectors/validation/ValidationInspector.tsx
@@ -11,10 +11,11 @@ import {
type SchemaType,
type ValidationMarker,
} from '@sanity/types'
-import {Box, Card, type CardTone, ErrorBoundary, Flex, Stack, Text} from '@sanity/ui'
+import {Box, Card, type CardTone, Flex, Stack, Text} from '@sanity/ui'
import {createElement, type ErrorInfo, Fragment, useCallback, useMemo, useState} from 'react'
import {type DocumentInspectorProps, useTranslation} from 'sanity'
+import {ErrorBoundary} from '../../../../../ui-components'
import {DocumentInspectorHeader} from '../../documentInspector'
import {useDocumentPane} from '../../useDocumentPane'
import {getPathTitles} from './getPathTitles'
diff --git a/packages/sanity/src/ui-components/errorBoundary/ErrorBoundary.tsx b/packages/sanity/src/ui-components/errorBoundary/ErrorBoundary.tsx
new file mode 100644
index 00000000000..09c0583f66f
--- /dev/null
+++ b/packages/sanity/src/ui-components/errorBoundary/ErrorBoundary.tsx
@@ -0,0 +1,32 @@
+import {
+ // eslint-disable-next-line no-restricted-imports
+ ErrorBoundary as UIErrorBoundary,
+ type ErrorBoundaryProps as UIErrorBoundaryProps,
+} from '@sanity/ui'
+import {useCallback, useContext} from 'react'
+
+import {SourceContext} from '../../_singletons'
+
+export type ErrorBoundaryProps = UIErrorBoundaryProps
+
+/**
+ * ErrorBoundary component that catches errors and uses onUncaughtError config property
+ * It also calls the onCatch prop if it exists.
+ */
+export function ErrorBoundary({onCatch, ...rest}: ErrorBoundaryProps): JSX.Element {
+ // Use context, because source could be undefined and we don't want to throw in that case
+ const source = useContext(SourceContext)
+
+ const handleCatch = useCallback(
+ ({error: caughtError, info: caughtInfo}: {error: Error; info: React.ErrorInfo}) => {
+ // Send the error to the source if it has an onUncaughtError method
+ source?.onUncaughtError?.(caughtError, caughtInfo)
+
+ // Call the onCatch prop if it exists
+ onCatch?.({error: caughtError, info: caughtInfo})
+ },
+ [source, onCatch],
+ )
+
+ return
+}
diff --git a/packages/sanity/src/ui-components/errorBoundary/__test__/ErrorBoundary.test.tsx b/packages/sanity/src/ui-components/errorBoundary/__test__/ErrorBoundary.test.tsx
new file mode 100644
index 00000000000..988d0a43bb3
--- /dev/null
+++ b/packages/sanity/src/ui-components/errorBoundary/__test__/ErrorBoundary.test.tsx
@@ -0,0 +1,87 @@
+import {beforeAll, describe, expect, it, jest} from '@jest/globals'
+import {studioTheme, ThemeProvider} from '@sanity/ui'
+import {render} from '@testing-library/react'
+import {type SanityClient} from 'sanity'
+
+import {createMockSanityClient} from '../../../../test/mocks/mockSanityClient'
+import {createTestProvider} from '../../../../test/testUtils/TestProvider'
+import {LocaleProviderBase, usEnglishLocale} from '../../../core/i18n'
+import {prepareI18n} from '../../../core/i18n/i18nConfig'
+import {ErrorBoundary} from '../ErrorBoundary'
+
+describe('ErrorBoundary', () => {
+ beforeAll(() => {
+ jest.clearAllMocks()
+ })
+
+ it('calls onUncaughtError when an error is caught', async () => {
+ const onUncaughtError = jest.fn()
+ const onCatch = jest.fn()
+
+ const ThrowErrorComponent = () => {
+ throw new Error('An EXPECTED, testing error occurred!')
+ }
+
+ const client = createMockSanityClient() as unknown as SanityClient
+
+ const TestProvider = await createTestProvider({
+ client,
+ config: {
+ name: 'default',
+ projectId: 'test',
+ dataset: 'test',
+ onUncaughtError,
+ },
+ })
+
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(onUncaughtError).toHaveBeenCalledTimes(1)
+ })
+
+ it('calls onCatch prop when an error is caught when no onUncaughtError exists', () => {
+ const onCatch = jest.fn()
+
+ const WrapperWithoutError = ({children}: {children: React.ReactNode}) => {
+ const locales = [usEnglishLocale]
+ const {i18next} = prepareI18n({
+ projectId: 'test',
+ dataset: 'test',
+ name: 'test',
+ })
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+
+ const ThrowErrorComponent = () => {
+ throw new Error('An EXPECTED, testing error occurred!')
+ }
+
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(onCatch).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/packages/sanity/src/ui-components/errorBoundary/index.ts b/packages/sanity/src/ui-components/errorBoundary/index.ts
new file mode 100644
index 00000000000..2bca71da107
--- /dev/null
+++ b/packages/sanity/src/ui-components/errorBoundary/index.ts
@@ -0,0 +1 @@
+export * from './ErrorBoundary'
diff --git a/packages/sanity/src/ui-components/index.ts b/packages/sanity/src/ui-components/index.ts
index 8e1eeedabc2..a0d7cbccdd9 100644
--- a/packages/sanity/src/ui-components/index.ts
+++ b/packages/sanity/src/ui-components/index.ts
@@ -1,6 +1,7 @@
export * from './button'
export * from './conditionalWrapper'
export * from './dialog'
+export * from './errorBoundary'
export * from './menuButton'
export * from './menuGroup'
export * from './menuItem'