ScreenshotNeo

BlogHow-to

How to Fix Cypress TypeError: form.submit Is Not a Function

A form control named or identified as submit can hide HTMLFormElement.submit(). Find the collision, rename it, and use requestSubmit() when needed.

By the ScreenshotNeo team30 September 20267 min read

How to Fix Cypress TypeError: form.submit Is Not a Function

Short answer: Cypress is usually exposing a DOM naming collision, not causing a Cypress defect. If a form control has name="submit" or id="submit", that control can mask the form’s native submit() method. Rename or remove the conflicting attribute. If your code needs normal browser submission behavior, call form.requestSubmit() instead.

MDN documents the collision directly: a form control named or identified as submit masks HTMLFormElement.submit(). MDN: HTMLFormElement.submit() explains the masking and the behavioral difference between submit() and requestSubmit().

1. Find the collision

Start with the failing stack-trace line. If it looks like form.submit(), inspect the actual form rendered in the browser, not only the component source.

A control named or identified as submit can hide the form’s native method.
A control named or identified as submit can hide the form’s native method.
<form id="profile-form">
  <input name="email" type="email" required>
  <button id="submit" type="submit">Save</button>
</form>

<script>
  const form = document.querySelector('#profile-form');
  form.submit(); // TypeError: form.submit is not a function
</script>

Here, form.submit resolves to the button with id="submit", so it is an element rather than a callable method.

Check every descendant control for either attribute:

const form = document.querySelector('#profile-form');

console.log(form);
console.log('form.submit value:', form.submit);
console.log('submit type:', typeof form.submit);

for (const control of form.elements) {
  if (control.id === 'submit' || control.getAttribute('name') === 'submit') {
    console.log('Collision:', control);
  }
}

You can also search the project:

rg -n '(^|\s)(id|name)=["'"']submit["'"']|\bsubmit\s*:' src cypress

2. Apply the markup fix

Rename the control to a name that describes its purpose, or remove the ID if scripts and labels do not need it.

<form id="profile-form">
  <input name="email" type="email" required>
  <button id="save-profile" name="save-profile" type="submit">Save</button>
</form>

After this change, form.submit resolves to the native function again. Prefer descriptive identifiers such as save-profile, continue-checkout, or delete-account.

3. Choose between submit() and requestSubmit()

API Constraint validation submit event and handlers Submitter button
form.submit() Skipped Not dispatched No
form.requestSubmit() Runs Dispatched Optional

Use requestSubmit() when you want the same path as activating a submit button: browser validation, the submit event, and application handlers all run. Use the optional submitter argument when a particular button controls behavior such as a different formaction or a named action.

const form = document.querySelector('#profile-form');
form.requestSubmit();

const saveButton = form.querySelector('[name="save-profile"]');
form.requestSubmit(saveButton);

The submitter must be a submit button that belongs to that form. Passing an unrelated element raises an error. See MDN: requestSubmit().

Use direct submission only when deliberately bypassing validation and the submit event:

const form = document.querySelector('#profile-form');
HTMLFormElement.prototype.submit.call(form);

The prototype call can bypass a shadowing control, but it does not change the semantics of submit(): validation and the submit event are still skipped. Fix the markup and choose the API whose behavior your application requires.

4. Reproduce and fix it in Cypress

Cypress documents that typing {enter} into an input associated with a form can cause implicit form submission and may synthesize a click on the form's submit button. See Cypress type() documentation. That interaction can reach application code containing the collision; it does not create the name or id.

Cypress can trigger the application’s implicit form-submission path when a test types Enter.
Cypress can trigger the application’s implicit form-submission path when a test types Enter.
describe('profile form', () => {
  it('submits when Enter is pressed', () => {
    cy.visit('/profile');
    cy.get('#profile-form input[name="email"]')
      .type('dev@example.com{enter}');

    cy.get('#profile-form').should('have.attr', 'data-submitted', 'true');
  });
});

If your application calls a submission method in a handler, assert the rendered markup before exercising the path:

cy.get('#profile-form').within(() => {
  cy.get('[name="submit"], #submit').should('not.exist');
  cy.get('button[type="submit"]').should('exist');
});

When the form intentionally uses a submit button, target it explicitly:

cy.get('#save-profile').click();
// or, for keyboard behavior:
cy.get('#profile-form input[name="email"]').type('{enter}');

If a submit handler calls preventDefault(), the browser will not navigate or perform the default action. Cypress can still observe the event and your application can still make an XHR or update state.

5. A reliable debugging sequence

  1. Read the stack trace and identify the exact line calling submit().
  2. Pause in DevTools and inspect the form object and typeof form.submit.
  3. List form.elements and look for name="submit" or id="submit".
  4. Search templates, JSX, server-rendered HTML, and component libraries for both attributes.
  5. Rename the control or remove the unnecessary ID.
  6. Replace direct submit() with requestSubmit() when validation and submit handlers are required.
  7. Rerun the Cypress test that types Enter or clicks the submit control.

6. Common errors and their fixes

form.submit is not a function

Cause: A descendant control masks the method. Fix: Rename or remove its name or id, then verify typeof form.submit === 'function'.

Cannot read properties of null

Cause: The selector did not find the form, often because Cypress queried before the page rendered or used a stale ID. Fix: Wait for the page state, use a stable selector, and assert the form exists before accessing it.

requestSubmit throws a submitter error

Cause: The argument is not a submit button belonging to that form. Fix: Select button[type="submit"] or an input with type="submit" from the same form, or omit the argument.

The test submits but the page does not navigate

Cause: A handler called preventDefault(), or the application uses an asynchronous request. Fix: Assert the request or resulting UI state instead of requiring navigation.

The error appears only in one environment

Cause: Server-rendered markup, feature flags, or a component variant adds the conflicting control in that environment. Fix: Capture the rendered HTML in the failing environment and compare the form descendants, IDs, and names.

7. Reliability and test design

Use stable, purpose-specific selectors such as data-cy="save-profile" for Cypress. Keep those selectors separate from DOM method names. Test both valid and invalid input so the suite verifies whether constraint validation is intentionally part of the flow.

<button data-cy="save-profile" type="submit">Save</button>

For deterministic tests, wait on the application state that matters:

cy.intercept('POST', '/api/profile').as('saveProfile');
cy.get('[data-cy="save-profile"]').click();
cy.wait('@saveProfile').its('response.statusCode').should('eq', 200);

Avoid adding arbitrary delays to hide the collision. Delays do not change property lookup and make failures slower. Fix the rendered DOM and use the correct native API.

8. Or skip the browser setup

If your workflow needs screenshots of the resulting page rather than an interactive Cypress browser, ScreenshotNeo can capture a URL with one request. Its API accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. Responses identify the result with X-Page-Verdict and X-Billed headers. An MCP server also lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

The service supports full-page and element captures, device presets, custom viewports, dark mode, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, bulk capture, and usage reporting. Failed loads and cache hits cost nothing. The Free plan includes 1,000 shots each month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

9. Performance and cost notes

  • Fixing the collision removes an immediate JavaScript exception and avoids retries caused by a broken submit path.
  • requestSubmit() may run validation and handlers, so its work can be greater than direct submit(); that is usually the behavior users expect.
  • In Cypress, wait on network aliases or visible state rather than fixed sleeps.
  • For screenshot automation, use an appropriate wait condition, block unnecessary resources when safe, and choose caching when repeated captures are acceptable.
  • ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits are free. Plans include Free 1,000/month, Starter $5/3,000, Growth $15/15,000, Pro $39/60,000, Scale $99/250,000, and Business $249/1,000,000. Yearly billing gives two months free.

FAQ

Is this a Cypress bug?

No. Cypress can trigger the form path, especially through implicit Enter submission, but the collision is in the page's rendered DOM.

Can I keep id="submit" for styling?

Rename it. CSS can target a descriptive class or data attribute without masking the form method.

Should every call to submit() become requestSubmit()?

No. Change it when validation and submit-event behavior are desired. Keep direct submission only when bypassing those behaviors is deliberate.

Why did a button with only name="submit" cause the error?

Form controls are exposed as named properties on the form, so either a matching name or id can shadow the method.