I built my portfolio site as my first JavaScript project, working with an AI design partner for review. The live site and the repo are public. It has a contact form with custom validation: my own code checks each field and writes its own error message under it. This post is about one bug in that form, and the wrong place I went looking for it.
A few days after I called the site finished, I was testing it on my phone. I typed an invalid email address, one with the wrong format, and pressed send. The message that appeared was not mine. It was the browser's own small tooltip, in the browser's wording and style.
I had written code for exactly this case, so I tried an empty email field to see whether my messages were broken everywhere or only in this one situation. With the field empty, my message appeared, in my wording, under the field.
At the time I read that as pointing at my format check. Empty worked, wrong format didn't, so the format code must be the problem.
My isEmail() function has three branches: empty, wrong format, and valid. The wrong-format branch was the one not showing, so I read through it. I read validate() as well. Everything looked correct, and I couldn't find anything wrong with either one.
I worked out that my code wasn't wrong. It wasn't running at all. My validation lives in a submit handler on the form:
form.addEventListener('submit', (event) => {
event.preventDefault();
// validate name, message and email, then send
});
When someone presses the submit button, the browser runs its own validation first, using the rules built into each input. An <input type="email"> has a built-in rule about format. If a field breaks a rule, the browser stops the submission, shows its own message, and never fires the submit event. My handler, and everything inside it, never got a chance.
The empty-field case behaved differently for a reason I only understood afterward. At that point none of my fields had the required attribute, and an empty email input without required doesn't break any browser rule. So the browser let the submission through, the submit event fired, and my code ran and showed my message.
<form action="..." method="post" novalidate>
novalidate tells the browser not to run its automatic checks when the form is submitted. My submit handler now fires every time, and my own messages appear. It doesn't switch validation off for code I call myself: isEmail() uses email.checkValidity() directly, and that works the same as before.
Later, during an accessibility review of the site, I noticed that none of the three fields had required. A screen reader user would only find out a field was mandatory after submitting and getting an error. I wanted to add it back, but I was worried it would bring the browser's messages back with it.
It doesn't, because novalidate and required do different jobs. novalidate stops the browser from blocking the submission and showing its own tooltip. required also tells assistive technology that a field is mandatory, and novalidate leaves that part alone. So I added required to all three fields and kept novalidate, and the form is still handled by my own code.
I spent my time reading code that was correct. The check I skipped was whether the handler was running at all. If my code isn't producing the output I expect, the first question should be whether it ran, before asking whether it's right.