actualbudget/actual · error
Unknown modal
Error message
Unknown modal
What it means
Modals maps the current modalStack entries to components via a switch on the modal name. If a modal in the stack has a name without a registered case (and no default fallback component), it throws 'Unknown modal'. This indicates the modal stack contains an entry the renderer doesn't know — usually a newly added modal or a renamed one missing its case here.
Source
Thrown at packages/desktop-client/src/components/Modals.tsx:435
return <OutOfSyncMigrationsModal key={key} />;
case 'edit-access':
return <EditUserAccess key={key} {...modal.options} />;
case 'edit-user':
return <EditUserFinanceApp key={key} {...modal.options} />;
case 'transfer-ownership':
return <TransferOwnership key={key} {...modal.options} />;
case 'enable-openid':
return <OpenIDEnableModal key={key} {...modal.options} />;
case 'enable-password-auth':
return <PasswordEnableModal key={key} {...modal.options} />;
default:
throw new Error('Unknown modal');
}
})
.map((modal, idx) => (
<Fragment key={`${modalStack[idx].name}-${idx}`}>{modal}</Fragment>
));
// fragment needed per TS types
// oxlint-disable-next-line react/jsx-no-useless-fragment
return <>{modals}</>;
}
View on GitHub (pinned to d4334cb6e6)
Solutions
- Add a case for the missing modal name in Modals.tsx returning its component with modal.options.
- Verify the exact name being pushed matches the case string (check for typos/renames).
- Clear the stale modal state (reload the app / reset the modal stack) if an outdated name was persisted.
- Grep for pushModal calls with that name to find the source dispatching it.
- Add a type for allowed modal names so mismatches are caught at compile time.
Example fix
// before
case 'enable-password-auth':
return <PasswordEnableModal key={key} {...modal.options} />;
default:
throw new Error('Unknown modal');
// after
case 'enable-password-auth':
return <PasswordEnableModal key={key} {...modal.options} />;
case 'my-new-modal':
return <MyNewModal key={key} {...modal.options} />;
default:
throw new Error('Unknown modal'); Defensive patterns
Strategy: type-guard
Validate before calling
const KNOWN_MODALS = ['openid-enable','enable-password-auth','add-account','schedule-edit'] as const;
type ModalName = typeof KNOWN_MODALS[number];
function isKnownModal(name) {
return KNOWN_MODALS.includes(name);
}
// guard before pushModal:
if (!isKnownModal(modal.name)) return; Type guard
function isModalName(value) {
return typeof value === 'string' && KNOWN_MODALS.includes(value);
} Try / catch
// Render-time throws can't be caught inside the component; guard at dispatch time:
try {
dispatch(pushModal({ modal: { name: modalName, options } }));
} catch (e) {
if (e.message === 'Unknown modal') {
logger.warn('Attempted to open unregistered modal', modalName);
return;
}
throw e;
} Prevention
- Centralize modal names in a shared union type and use it for both pushModal calls and the Modals switch.
- Always add the Modals.tsx case in the same PR that introduces a new modal.
- Add an ErrorBoundary around <Modals /> so an unknown modal doesn't blank the whole app.
- Grep pushModal call sites when renaming a modal.
When it happens
Trigger: Pushing a modal with a name that has no case in Modals.tsx (e.g. dispatch(pushModal({ modal: { name: 'my-modal' } })) before adding the mapping); renaming a modal in one place but not the Modals switch; a typo in the modal name string.
Common situations: Adding a new modal feature and forgetting the Modals.tsx case; persisted client state re-pushing a removed modal name; version upgrade where a modal was renamed; typo in a pushModal call.
Related errors
- Unrecognized menu option: ${String(item)}
- Unhandled edit field name: ${String(name)}
- Unknown budget action type: ${String(type)}
- Unrecognized menu option: ${String(name)}
- Unrecognized menu item: ${name}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/7ee5c3c6278d3542.
Report an issue: GitHub.