skills/pimcore-studio-ui-error-handling/SKILL.md
Error handling patterns in Pimcore Studio UI - trackError, ApiError, GeneralError, ErrorBoundary, and proper error flow
npx skillsauth add pimcore/skills pimcore-studio-ui-error-handlingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Error handling in Pimcore Studio UI: trackError for centralized reporting, ApiError/GeneralError wrappers, ErrorBoundary for render errors, and the standard reactive pattern for RTK Query.
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
import { trackError, ApiError, GeneralError, isApiErrorData, ErrorBoundary } from '@pimcore/studio-ui-bundle/modules/app'
| Symbol | Purpose |
|--------|---------|
| trackError(provider, handler?) | Centralized error display. Deduplicates errors within the same execution cycle. |
| ApiError(error) | Wraps RTK Query / structured API errors. Extracts errorKey and renders localized messages via error.${errorKey} translation keys. |
| GeneralError(message) | Wraps simple string errors. Throws after display — stops execution. |
| isApiErrorData(unknown) | Type guard for unknown error shapes. Returns true if object matches API error structure. |
| ErrorBoundary | React component that catches render errors. Optional fallback prop. |
ApiError or GeneralErrorErrorModalServicetrackError(new GeneralError(...)) does not execute| Scenario | Use |
|----------|-----|
| RTK Query error from a query or mutation | ApiError |
| Axios/fetch error with structured response data | ApiError({ data: error.response.data }) |
| Network failure / no response | GeneralError |
| Validation failure in client code | GeneralError |
| Missing required configuration / unexpected state | GeneralError |
This is the only correct way to handle RTK Query errors in Pimcore Studio. Use the error state from the hook with useEffect and trackError.
import { useAssetUpdateMutation } from '@pimcore/studio-ui-bundle/api/asset'
import { trackError, ApiError } from '@pimcore/studio-ui-bundle/modules/app'
import { useMessage } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from 'react-i18next'
import { useEffect } from 'react'
import { isNil } from 'lodash'
const AssetEditor = ({ asset }: { asset: Asset }): React.JSX.Element => {
const { t } = useTranslation()
const messageApi = useMessage()
const [updateAsset, { data, error }] = useAssetUpdateMutation()
// Track errors
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
// Handle success separately
useEffect(() => {
if (!isNil(data)) {
void messageApi.success(t('save-success'))
}
}, [data])
const handleSave = (values: FormValues): void => {
updateAsset({ id: asset.id, body: values })
}
return <Form onFinish={ handleSave }>{/* fields */}</Form>
}
Why this pattern:
trackError deduplicates and provides consistent UXUse a separate useEffect per mutation error. Combining them into one effect causes stale closures and missed updates.
const [createAsset, { error: createError }] = useAssetCreateMutation()
const [updateAsset, { error: updateError }] = useAssetUpdateMutation()
useEffect(() => {
if (!isNil(createError)) trackError(new ApiError(createError))
}, [createError])
useEffect(() => {
if (!isNil(updateError)) trackError(new ApiError(updateError))
}, [updateError])
When the API returns:
{
"errorKey": "asset.not-found",
"message": "Asset with ID 123 was not found"
}
ApiError looks up the translation key error.asset.not-found. If the translation exists, it displays the localized message. Otherwise, it falls back to the raw message from the response.
Use GeneralError when there is no API response — validation failures, missing config, unexpected state. Remember it throws after display.
import { trackError, GeneralError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const processElement = (element: Element | undefined): void => {
if (isNil(element)) {
trackError(new GeneralError('Cannot process: element is undefined'))
// throws — code below is unreachable
}
element.update()
}
Outside RTK Query, convert errors manually. Use ApiError when you have a structured response, GeneralError for network failures.
import { trackError, ApiError, GeneralError } from '@pimcore/studio-ui-bundle/modules/app'
import { isNil } from 'lodash'
const uploadFile = async (url: string, data: FormData): Promise<void> => {
try {
await axios.post(url, data)
} catch (error: any) {
if (!isNil(error.response)) {
trackError(new ApiError({ data: error.response.data }))
} else {
trackError(new GeneralError('Network error'))
}
}
}
Catches React render errors and prevents the entire app from crashing.
import { ErrorBoundary } from '@pimcore/studio-ui-bundle/modules/app'
<ErrorBoundary fallback={ <div>Something went wrong</div> }>
<MyComponent />
</ErrorBoundary>
Important: The widget container wraps every widget in an ErrorBoundary automatically. Do not add an ErrorBoundary around your widget root. Add your own only for isolated component trees that should fail independently (e.g., a third-party chart, a complex feature panel).
Use when you receive an error from a non-RTK-Query source and need to determine its shape.
import { trackError, ApiError, GeneralError, isApiErrorData } from '@pimcore/studio-ui-bundle/modules/app'
const handleExternalError = (error: unknown): void => {
if (isApiErrorData(error)) {
trackError(new ApiError(error))
} else if (error instanceof Error) {
trackError(new GeneralError(error.message))
} else {
trackError(new GeneralError('An unknown error occurred'))
}
}
The .unwrap() pattern from generic Redux Toolkit usage is wrong in Pimcore Studio.
// ❌ WRONG
const handleSave = async (): Promise<void> => {
try {
await updateAsset({ id, body }).unwrap()
message.success('Saved')
} catch (error) {
message.error('Save failed')
}
}
Problems:
trackError — no centralized error handling, no error modalAlways use the useEffect + error state pattern shown above.
trackError already shows the error modal. Do not also show a toast for the same error.
// ❌ WRONG — duplicate display
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
messageApi.error('Operation failed') // redundant!
}
}, [error])
// ✅ CORRECT
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
// ❌ WRONG — error could be undefined on mount
useEffect(() => {
trackError(new ApiError(error))
}, [error])
// ✅ CORRECT
useEffect(() => {
if (!isNil(error)) {
trackError(new ApiError(error))
}
}, [error])
// ❌ WRONG — user never sees this
console.error('API call failed:', error)
// ✅ CORRECT
trackError(new ApiError(error))
// ❌ WRONG — stale closures, missed updates
useEffect(() => {
if (!isNil(createError)) trackError(new ApiError(createError))
if (!isNil(updateError)) trackError(new ApiError(updateError))
}, [createError, updateError])
// ✅ CORRECT — separate useEffect per error
useEffect(() => {
if (!isNil(createError)) trackError(new ApiError(createError))
}, [createError])
useEffect(() => {
if (!isNil(updateError)) trackError(new ApiError(updateError))
}, [updateError])
// ❌ WRONG — widget container already provides ErrorBoundary
<ErrorBoundary fallback={ <div>Error</div> }>
<MyWidget />
</ErrorBoundary>
// ✅ CORRECT — just render the widget
<MyWidget />
tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality