Skip to content

(fix) O3-4681: Handle missing subforms gracefully to prevent null crash#703

Open
vivektarun wants to merge 2 commits into
openmrs:mainfrom
vivektarun:fix/O3-4681-handle-missing-subform-gracefully
Open

(fix) O3-4681: Handle missing subforms gracefully to prevent null crash#703
vivektarun wants to merge 2 commits into
openmrs:mainfrom
vivektarun:fix/O3-4681-handle-missing-subform-gracefully

Conversation

@vivektarun

Copy link
Copy Markdown

Summary

Fixes a crash where forms containing subform pages (isSubform: true)
referencing a form that does not exist on the backend threw:

TypeError: Cannot read properties of null (reading 'markdown')

making the entire form unusable with a white screen error.

Related Issue

Fixes O3-4681

Root Cause

When fetchOpenMRSForm returned null for a missing subform:

  1. Promise.all in loadSubForms crashed the entire form load
  2. refinedFormJson became null but no null guard existed
  3. Multiple components tried to access .sections on subform
    pages which have no sections property

Changes

File Change
useFormJson.ts Promise.allSettled + null guards
form-engine.component.tsx null guard for refinedFormJson
encounter-form-processor.ts null guard for page.sections
page.renderer.component.tsx InlineNotification for missing subform
common-utils.ts Array.from(page.sections ?? [])
useFormJson.test.ts new test for missing subform scenario

Before

Screen.Recording.2026-03-15.at.12.52.52.AM.1.mov
  • White screen crash
  • TypeError: Cannot read properties of null (reading 'markdown')
  • Form completely unusable

After

Screen.Recording.2026-03-15.at.2.48.48.PM.mov
  • Parent form renders correctly
  • Missing subform page shows helpful error:
    "Subform could not be loaded. Please verify the form name
    is correct and that it is published."
  • Other pages render normally
  • Helpful console message logged

Screenshots

Tests Passing

image

How to Test

  1. Open form-builder
  2. Create a new form
  3. Paste this schema in the schema editor:
{
  "name": "Test Subform Parent",
  "version": "1",
  "published": false,
  "retired": false,
  "referencedForms": [],
  "allowUnspecifiedAll": true,
  "pages": [
    {
      "label": "Lab Order",
      "isSubform": true,
      "subform": {
        "name": "HMIS LAB 001:General Laboratory Test"
      }
    },
    {
      "label": "Main Section",
      "sections": [
        {
          "label": "Test Section",
          "isExpanded": true,
          "questions": [
            {
              "label": "Notes",
              "type": "obs",
              "id": "notes",
              "questionOptions": {
                "rendering": "textarea",
                "concept": "160632AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
              }
            }
          ]
        }
      ]
    }
  ]
}
  1. Click Render Changes
  2. Confirm:
    • ✅ No crash
    • ✅ Lab Order page shows red error notification
    • ✅ Main Section renders with Notes field
    • ✅ Console shows helpful error message

Checklist

  • Fixes the reported crash
  • Unit test added for missing subform scenario
  • All existing tests pass (7/7)
  • Tested locally against dev3.openmrs.org
  • PR title follows conventional commit format
  • No breaking changes to existing functionality

Problem:
When a form contained a subform page (isSubform: true) referencing
a form that does not exist on the backend, the form engine crashed
with: TypeError: Cannot read properties of null (reading 'markdown')
making the entire form unusable.

Changes:
- useFormJson.ts: Replace Promise.all with Promise.allSettled in
  loadSubForms so a single missing subform does not crash the parent
  form. Add null guard in updateFormJsonWithSubForms.

- form-engine.component.tsx: Add null guard for refinedFormJson
  before rendering to prevent crash when form loading fails.

- encounter-form-processor.ts: Add null guard for page.sections
  in prepareFormSchema and processFields to safely skip subform
  pages that have no sections.

- page.renderer.component.tsx: Show an InlineNotification error
  message when a subform page fails to load instead of crashing.
  Added page.sections ?? [] guard to safely handle missing sections.

- common-utils.ts: Use Array.from(page.sections ?? []) to prevent
  'undefined is not iterable' crash on subform pages.

- useFormJson.test.ts: Add test case verifying that the parent form
  loads correctly and logs an error when a subform cannot be found.

Before: Form crashed with null reference error, white screen shown.
After:  Parent form renders correctly. Missing subform page shows a
        helpful error: 'Subform could not be loaded. Please verify
        the form name is correct and that it is published.'
@samuelmale

Copy link
Copy Markdown
Member

Thanks for working on this @vivektarun! I think beyond this PR, we should ensure that a subform doesn't create its own Encounter or Visit.

@vivektarun

vivektarun commented Mar 16, 2026

Copy link
Copy Markdown
Author

Hi @samuelmale, thank you for the feedback!

I understand the concern about subforms creating their own Encounter. Let me share my understanding of the problem and the proposed fix.


Current wrong behaviour

When a subform is loaded and the doctor submits the form, FormProcessorFactory is instantiated twice — once for the parent form and once for the subform. The subform instance has no reference to the parent's encounter, so it calls saveEncounter() independently and creates a brand new Encounter in the database. This results in two separate Encounter records for a single form submission — one holding the parent form observations and one holding the subform observations. When the doctor opens the form next week to edit, the form loads only the parent encounter and the subform fields appear empty because their observations are stored under a completely different encounter that the form has no way of finding.


Proposed correct behaviour

The subform should never create its own Encounter. Instead:

  1. form-renderer.component.tsx should pass the parent form's existing encounter (context.domainObjectValue) down to the subform's FormProcessorFactory as a prop
  2. form-processor-factory.component.tsx should accept this parentEncounter prop and inject it into the subform's processorContext as domainObjectValue
  3. encounter-form-processor.ts should detect that isSubform is true and domainObjectValue already exists, and skip creating a new encounter — instead saving all subform observations under the parent encounter's uuid

This way one single Encounter holds all observations from both the parent form and the subform, and editing works correctly next time.


I have attached three diagrams below showing the wrong flow, the correct flow, and the database state comparison:

2026-03-16_15-24-34 2026-03-16_15-24-40 2026-03-16_15-24-48

A few questions before I start coding this:

  1. Is this the right approach or does the team have a different design in mind for how subforms should share the parent encounter?
  2. Should this be addressed in this same PR or opened as a separate issue since it is a different concern from the null crash fix?
  3. Is there an existing pattern in the codebase where a child processor shares a parent's domain object that I can reference?

Happy to implement this if the approach looks correct. Thank you!

@ibacher ibacher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @vivektarun. I think most of my remarks are about comments here.

Comment on lines +87 to +88
// Guard: skip subform pages that have no sections
// (subform failed to load or is still a reference placeholder)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is inaccurate as there's nothing in the code as written that makes this applicable to subforms.

Comment thread src/form-engine.component.tsx Outdated
}

export default I18FormEngine;
export default I18FormEngine; No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export default I18FormEngine;
export default I18FormEngine;

Comment thread src/hooks/useFormJson.ts Outdated
!isTrue(page.isHidden) &&
page.subform?.form?.encounterType === formJson.encounterType
) {
// Guard: skip if subform.form failed to load (is null/undefined)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this comment is particularly helpful

Comment thread src/hooks/useFormJson.ts Outdated
* @param subForms - Array of loaded subform schemas
*/
function updateFormJsonWithSubForms(formJson: FormSchema, subForms: FormSchema[]): void {
// First populate successfully loaded subforms

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

????

Comment thread src/hooks/useFormJson.ts Outdated

/**
* Updates the parent form JSON with the loaded subforms.
* Includes a null guard to safely skip any undefined subform entries.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this second line.

Comment thread src/hooks/useFormJson.test.tsx Outdated
Comment on lines +189 to +191
// Build a raw parent form that references a subform that does not exist on the backend.
// This simulates the exact scenario from O3-4681 where a subform name is wrong or
// the subform has not been published yet on the target server.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first line of this comment is ok. The rest of it should be excised.

Comment on lines +81 to +83
// Show a clear error message when the subform could not be loaded.
// This helps admins/developers immediately understand what went wrong
// instead of seeing a blank page or a cryptic crash.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this comment explain something a programmer wouldn't already understand?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further note that although you may want to make this visible to admins or developers, like most error messages, this is probably mostly going to be seen by users who are neither. It's likely more helpful to say something like "Contact your system administrator."

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right on both points. The comment is self-evident from the code, so that I will remove it. I will also update the message to say something like 'Contact your system administrator' since end users will mostly see this.

subtitle={t(
'subformLoadErrorMessage',
'The subform "{{subformName}}" could not be found on this server. Please verify the form name is correct and that it is published.',
{ subformName: page.subform?.name },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 'The subform "undefined" could not be found on this server.' a message we want to display?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that is a bad user experience. I will add a guard so that if page.subform?.name is undefined it falls back to a generic message without showing the word undefined.

Comment on lines +35 to +36
// Guard: subform pages have no sections — return empty array
// so the rest of the component renders safely

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be excised.

Comment on lines +29 to +30
// If this page is a subform page but the subform failed to load,
// show a helpful inline error instead of crashing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that this comment is helpful.

- remove unhelpful inline comments
- update formSessionIntent JSDoc
- include subform name in error log
- use type predicate instead of cast
- fix undefined in error message
- update error message for end users
@vivektarun

vivektarun commented Mar 16, 2026

Copy link
Copy Markdown
Author

Hi @ibacher, thank you for the detailed review! I have addressed all the comments. Here is a summary of the changes made:

  1. Removed all unhelpful inline comments that were self-evident from the code itself.

  2. Updated the formSessionIntent JSDoc in both loadFormJson and loadSubForms to explain that it is used to apply a specific intent filter to the subform schema when a form has multiple behaviours defined.

  3. Updated the error log in loadSubForms to include the name of the subform that failed to load so it is clear which one could not be resolved.

  4. Replaced the explicit cast in Promise.allSettled with a type predicate in the filter so TypeScript infers the correct type without needing a cast.

  5. Fixed the error message in page.renderer.component.tsx to guard against showing the word "undefined" when page.subform.name is not set, and updated the message to say "Contact your system administrator" since this will mostly be seen by end users rather than admins or developers.

All test cases are running

2026-03-17_00-45-30

Please let me know if there is anything else to address. Thank you!

@vivektarun vivektarun requested a review from ibacher March 16, 2026 19:47
@vivektarun

Copy link
Copy Markdown
Author

@ibacher Would appreciate any feedback whenever you get time. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants