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

  1. Add a case for the missing modal name in Modals.tsx returning its component with modal.options.
  2. Verify the exact name being pushed matches the case string (check for typos/renames).
  3. Clear the stale modal state (reload the app / reset the modal stack) if an outdated name was persisted.
  4. Grep for pushModal calls with that name to find the source dispatching it.
  5. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/7ee5c3c6278d3542. Report an issue: GitHub.