actualbudget/actual · warning

Invalid catalog format: expected an array

Error message

Invalid catalog format: expected an array

What it means

After a successful fetch, fetchCatalog validates that the parsed JSON body is an array. If the endpoint returned valid JSON of another shape (object, null, string), it throws 'Invalid catalog format: expected an array'. This protects the theme picker from runtime crashes when rendering an unexpected payload.

Source

Thrown at packages/desktop-client/src/hooks/useThemeCatalog.ts:31

  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchCatalog = async () => {
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch(CATALOG_URL);

        if (!response.ok) {
          throw new Error(`Failed to fetch catalog: ${response.statusText}`);
        }

        const data = await response.json();

        // Validate that data is an array
        if (!Array.isArray(data)) {
          throw new Error('Invalid catalog format: expected an array');
        }

        setData(data);
      } catch (err) {
        setError(
          err instanceof Error ? err.message : 'Failed to load theme catalog',
        );
      } finally {
        setIsLoading(false);
      }
    };

    void fetchCatalog();
  }, []);

  return {
    data,
    isLoading,

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Verify the endpoint actually returns a top-level JSON array (curl the URL and inspect the body)
  2. If the API changed, update fetchCatalog to unwrap the envelope (e.g. data.themes)
  3. Harden the code: also validate the shape of individual catalog items before setData
  4. Point CATALOG_URL to the correct, current catalog URL

Example fix

// before
const data = await response.json();
if (!Array.isArray(data)) {
  throw new Error('Invalid catalog format: expected an array');
}
// after
const data = await response.json();
const themes = Array.isArray(data) ? data : Array.isArray(data?.themes) ? data.themes : null;
if (!themes) {
  throw new Error('Invalid catalog format: expected an array');
}
setData(themes);
Defensive patterns

Strategy: validation

Validate before calling

const isThemeArray = (data: unknown): data is ThemeCatalogItem[] =>
  Array.isArray(data) && data.every(t => !!t && typeof t === 'object' && typeof (t as any).name === 'string');

Type guard

const isThemeCatalog = (x: unknown): x is ThemeCatalogItem[] =>
  Array.isArray(x) && x.every(item =>
    typeof item === 'object' && item !== null &&
    typeof (item as any).repo === 'string' && typeof (item as any).name === 'string'
  );

Try / catch

const { catalog, error } = useThemeCatalog();
if (error?.includes('Invalid catalog format')) {
  // surface a schema-mismatch warning and use bundled themes only
  console.error('Catalog schema changed:', error);
}

Prevention

When it happens

Trigger: CATALOG_URL serves JSON that parses but is not an array — e.g. an object like { themes: [...] }, an HTML error page parsed as text, a null response, or an API version change that wrapped the array.

Common situations: The catalog endpoint was updated to a paginated/envelope response format; a proxy or captive portal returned a JSON error body; misconfigured CDN serving a stub file.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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