appsmithorg/appsmith · error · Error

FormControl compound components cannot be rendered outside t

Error message

FormControl compound components cannot be rendered outside the FormControl component

What it means

Intended to fire from useFormControlContext() in the Appsmith Design System when a FormControl compound component (e.g. FormLabel, FormHelperText, FormInput) is rendered without a FormControl ancestor. IMPORTANT DEFECT grounded in the source: FormControlContext is created with createContext({...}) passing a non-null default object ({isRequired:false,isDisabled:false,size:"sm"}), so useContext always returns a truthy value and the `if (!context)` guard is DEAD CODE — this error can never actually be thrown at runtime as written. The intent is real; the implementation does not achieve it.

Source

Thrown at app/client/packages/design-system/ads/src/FormControl/FormControl.context.ts:16

import { createContext, useContext } from "react";
import type { FormControlProps } from "./FormControl.types";

export const FormControlContext = createContext<
  Pick<FormControlProps, "isRequired" | "isDisabled" | "size">
>({
  isRequired: false,
  isDisabled: false,
  size: "sm",
});

export const useFormControlContext = () => {
  const context = useContext(FormControlContext);

  if (!context) {
    throw new Error(
      "FormControl compound components cannot be rendered outside the FormControl component",
    );
  }

  return context;
};

export const FormControlProvider = FormControlContext.Provider;

View on GitHub (pinned to 8cd9021c24)

Solutions

  1. Always render compound components inside <FormControl> so context is provided explicitly.
  2. Fix the underlying defect: pass null as the createContext default (createContext<Pick<...> | null>(null)) so the guard becomes live, matching the Sidebar pattern.
  3. If fixing the library, audit all consumers once the throw becomes reachable — currently any out-of-tree usage has been silently passing.
  4. Add a unit test rendering a compound component without a provider to lock in the intended behavior.

Example fix

// before (dead guard)
export const FormControlContext = createContext({ isRequired:false, isDisabled:false, size:"sm" });
export const useFormControlContext = () => {
  const context = useContext(FormControlContext);
  if (!context) { throw new Error(...); }  // unreachable
  return context;
};

// after (live guard)
export const FormControlContext = createContext<Pick<FormControlProps,"isRequired"|"isDisabled"|"size"> | null>(null);
export const useFormControlContext = () => {
  const context = useContext(FormControlContext);
  if (!context) { throw new Error("FormControl compound components cannot be rendered outside the FormControl component"); }
  return context;
};
Defensive patterns

Strategy: type-guard

Type guard

// NOTE: current default is non-null so this never throws; fix the lib first.
// After fix (createContext<T|null>(null)):
function hasFormControlContext<T>(ctx: T | null): ctx is T { return ctx !== null; }

Try / catch

// Wrap risky rendering; today this is dead code, so add the provider regardless.
try { return renderCompound(); } catch (e) {
  if (/FormControl compound components/i.test(e.message)) return <FormControl>{renderCompound()}</FormControl>;
  throw e;
}

Prevention

When it happens

Trigger: Render a FormControl.* subcomponent outside <FormControl>. Intended trigger only — in practice the non-null default suppresses the throw, so the component silently renders with default context values instead.

Common situations: Copying a Form subcomponent into a story or test without the FormControl wrapper; refactoring a form and hoisting a label out of FormControl; using the design system primitives standalone. Developer expects a clear error but gets silent default sizing.

Related errors


AI-assisted analysis of appsmithorg/appsmith@8cd9021c24 (2026-08-12). Data as JSON: /api/errors/a216d242e259a410. Report an issue: GitHub.