Error handling
Introduction
Section titled βIntroductionβWe are now checking for validation errors on the form, but the user experience feels a bit clunky.
For example, if the user submits a form with more than one error, they are shown multiple alerts for each mistake.
Letβs see if we can improve this. In this guide we will:
- Collect all errors into an object
- Create a helper function to add error messages to the object
- Use a loop to process each error
- update our user interface to show each form field that needs correcting
Collect errors into an object
Section titled βCollect errors into an objectβ-
Firstly, letβs add an object to collect all the errors into, together with a helper function to add error messages:
Open
index.js
and add the line highlighted in green below:index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();12 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = parseInt(document.querySelector("#hoursPerWeek").value);// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}34 collapsed lines// Check if the user has provided an email addressif (userEmail === "") {alert("Please enter your email address.");return;}// Check if the user has selected a levelif (userLevel === "") {alert("Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {alert("Please enter at least one hour of tuition.");return;}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {alert("Invalid level of study selected.");return;}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {alert(`You can only study a maximum of ${maxAllowedHours} hours per week.`);return;}console.log({ userEmail, userLevel, userHours });});// Calculate the total cost// Display the total cost to the user -
Next, we will replace all the
alert
messages with objects that we willpush
into theerrors
array.Carefully delete the lines highlighted in red below and replace them with those highlighted in green:
index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();12 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = parseInt(document.querySelector("#hoursPerWeek").value);// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {alert("Please enter your email address.");addError("email", "Please enter your email address.");return;}// Check if the user has selected a levelif (userLevel === "") {alert("Please select a level of study");addError("level", "Please select a level of study.");return;}// Check if the user has specified at least one hour of study// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {alert("Please enter at least one hour of tuition.");addError("hoursPerWeek", "Please enter at least one hour of tuition.");return;}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {alert("Invalid level of study selected.");addError("level", "Invalid level of study selected.");return;}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {alert(`You can only study a maximum of ${maxAllowedHours} hours per week.`);addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);return;}console.log({ userEmail, userLevel, userHours });console.log({ errors });});// Calculate the total cost// Display the total cost to the userWe no longer want to
return
from the function if there is an error at this point, as we want to collect all the errors before displaying them to the user. -
Letβs test the new error handling by submitting the form with some errors.
Fill in the form with some errors and submit it.
You should see all the errors are now collected into a single object in the console.
We can use this single object to display all the error messages to the user at once π.
Style input fields with errors
Section titled βStyle input fields with errorsβ-
The first thing we should do is style the input fields that have errors.
Open
index.css
and add the following CSS:index.css :root {--black: rgb(22, 22, 22);--white: rgb(255, 255, 255);--grey-50: rgb(250, 250, 250);--grey-300: rgb(192, 198, 201);--grey-500: rgb(88, 104, 112);--brand-light: rgb(255, 90, 58);--brand-dark: rgb(172, 62, 41);--sky-500: rgb(14 165 233);--box-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),0 4px 6px -4px rgb(0 0 0 / 0.1);--error-bkg: rgb(255, 241, 241);--error-placeholder: rgb(255, 157, 157);}75 collapsed lines* {color: var(--grey-500);box-sizing: border-box;font-family: Arial, Helvetica, sans-serif;margin: 0;}button {cursor: pointer;}.form-wrapper {display: flex;background-color: var(--grey-50);min-height: 100vh;}.form-section {width: calc(50% - 1px);display: flex;flex-grow: 1;flex-direction: column;justify-content: center;align-items: flex-start;gap: 2rem;padding: 2rem 4rem;}.form-section:first-of-type {border-right: 1px solid var(--grey-300);}.form-field {display: flex;flex-direction: column;gap: 0.5rem;align-items: flex-start;width: 100%;}.form-label {font-size: 1.25rem;}.form-input {background-color: var(--white);border-radius: 999px;border: 1px solid var(--grey-300);font-size: 1.5rem;padding: 0.75rem 1.5rem;font-weight: 300;color: var(--black);width: 100%;}.form-input::placeholder {color: var(--grey-300);}.form-input:focus {outline: none;outline: 2px solid var(--sky-500);box-shadow: var(--box-shadow-lg);}.mobile-display {display: none;}.submit-btn {border-radius: 1.75rem;margin: 0 auto;text-align: center;color: var(--white);border: 0;padding: 0.75rem 2rem;font-size: 1.5rem;background: linear-gradient(180deg,var(--brand-light) 0%,var(--brand-dark) 100%);transition: all 0.3s ease;outline: none;box-shadow: none;}.submit-btn:hover {outline: 1px solid var(--brand-dark);box-shadow: var(--box-shadow-lg);transition: all 0.3s ease;}.error-input {background-color: var(--error-bkg);color: var(--brand-dark);border-color: var(--brand-dark);}.error-input::placeholder {color: var(--error-placeholder);}.error-label {color: var(--brand-dark);}@media screen and (max-width: 640px) {.mobile-hidden {display: none;}.mobile-display {display: block;}}This CSS will simply style the input fields with errors with a red background, border, label and placeholder text.
Next, we will use JavaScript to apply this class to any input fields that contain errors.
-
Open
index.js
. Add the code highlighted below just above the finalconsole.log()
statement:index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();59 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (userHours === "") {addError("hoursPerWeek","Please specify how many hours of tuition you would like.");}if (userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}}console.log({ errors });});// Calculate the total cost// Display the total cost to the userTry this in your browser now.
You should see any input fields that have errors are now styled with a red background, border, label and placeholder text.
Display error messages
Section titled βDisplay error messagesβThe final step is to display the error messages to the user.
To do this, we will need to:
- update the HTML to include an empty
div
element for each error message - use JavaScript to populate these new
div
elements dynamically with the error messages.
-
Open
index.html
and look carefully for eachdiv
element with the classform-field
.Inside each of these, just below the
input
element, add an emptydiv
element with anid
attribute that closely matches theid
attribute of its correspondinginput
element:index.html 14 collapsed lines<!DOCTYPE html><html><head><title>Harmony | Sign Up</title><link rel="stylesheet" href="./index.css" /></head><body><form class="form-wrapper"><div class="form-section"><imgclass="mobile-display"src="./assets/harmony-music-academy-logo.svg"alt="Harmony Music Academy logo"/><div class="form-field"><label for="email" class="form-label">Email*</label><inputid="email"name="email"type="email"class="form-input"placeholder="Your email"/><div id="email-error"></div></div><div class="form-field"><label for="level" class="form-label">Level*</label><select id="level" name="level" class="form-input"><option value="basic">Basic</option><option value="advanced">Advanced</option></select><div id="level-error"></div></div><div class="form-field"><label for="hoursPerWeek" class="form-label">Hours per week*</label><inputid="hoursPerWeek"name="hours-per-week"type="number"class="form-input"placeholder="How many?"/><div id="hoursPerWeek-error"></div></div>15 collapsed lines<button type="submit" class="submit-btn">Sign up</button></div><div class="form-section mobile-hidden"><imgsrc="./assets/harmony-music-academy-logo.svg"alt="Harmony Music Academy logo"/></div></form><script type="module" src="./index.js"></script></body></html>We have added a
div
element with anid
attribute that matches theid
attribute of the correspondinginput
element. This will allow us to target thediv
element and populate it with the error message. -
Next letβs add the styles for this new element.
Open
index.css
, and add the code highlighted below, just below the other error stylings we added earlier:index.css 103 collapsed lines:root {--black: rgb(22, 22, 22);--white: rgb(255, 255, 255);--grey-50: rgb(250, 250, 250);--grey-300: rgb(192, 198, 201);--grey-500: rgb(88, 104, 112);--brand-light: rgb(255, 90, 58);--brand-dark: rgb(172, 62, 41);--sky-500: rgb(14 165 233);--box-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),0 4px 6px -4px rgb(0 0 0 / 0.1);--error-bkg: rgb(255, 241, 241);--error-placeholder: rgb(255, 157, 157);}* {color: var(--grey-500);box-sizing: border-box;font-family: Arial, Helvetica, sans-serif;margin: 0;}button {cursor: pointer;}.form-wrapper {display: flex;background-color: var(--grey-50);min-height: 100vh;}.form-section {width: calc(50% - 1px);display: flex;flex-grow: 1;flex-direction: column;justify-content: center;align-items: flex-start;gap: 2rem;padding: 2rem 4rem;}.form-section:first-of-type {border-right: 1px solid var(--grey-300);}.form-field {display: flex;flex-direction: column;gap: 0.5rem;align-items: flex-start;width: 100%;}.form-label {font-size: 1.25rem;}.form-input {background-color: var(--white);border-radius: 999px;border: 1px solid var(--grey-300);font-size: 1.5rem;padding: 0.75rem 1.5rem;font-weight: 300;color: var(--black);width: 100%;}.form-input::placeholder {color: var(--grey-300);}.form-input:focus {outline: none;outline: 2px solid var(--sky-500);box-shadow: var(--box-shadow-lg);}.mobile-display {display: none;}.submit-btn {border-radius: 1.75rem;margin: 0 auto;text-align: center;color: var(--white);border: 0;padding: 0.75rem 2rem;font-size: 1.5rem;background: linear-gradient(180deg,var(--brand-light) 0%,var(--brand-dark) 100%);transition: all 0.3s ease;outline: none;box-shadow: none;}.submit-btn:hover {outline: 1px solid var(--brand-dark);box-shadow: var(--box-shadow-lg);transition: all 0.3s ease;}.error-input {background-color: var(--error-bkg);color: var(--brand-dark);border-color: var(--brand-dark);}.error-input::placeholder {color: var(--error-placeholder);}.error-label {color: var(--brand-dark);}.error-message ul {padding-left: 0;}.error-message ul li {color: var(--brand-dark);font-weight: 800;font-size: 0.8rem;letter-spacing: 0.1px;list-style: none;margin-bottom: 0.5rem;}@media screen and (max-width: 640px) {.mobile-hidden {display: none;}.mobile-display {display: block;}}This CSS will style the error messages with a red text color, bold font weight, and a smaller font size.
-
Next, letβs add the JavaScript that will add this new class to any areas of the form that have errors.
index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();51 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}console.log({ userEmail, userLevel, userHours });console.log({ errors });});// Calculate the total cost// Display the total cost to the user -
Finally, we need to
return
early if there are any messages inside theerrors
object:index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();79 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}// Return early if there are any errorsif (Object.keys(errors).length > 0) {return;}console.log({ errors });});// Calculate the total cost// Display the total cost to the user
Test the form and commit changes
Section titled βTest the form and commit changesβCheck that the form now displays all errors in one place, and that the input fields are styled correctly.
If everything looks good, commit your changes using Github Desktop, and remember to push them to your Github reponsitory.
Summary
Section titled βSummaryβIn this guide, we have improved the error handling for the Harmony Music Academy form by:
- Collecting all errors into an object
- Creating a helper function to add error messages to the object
- Using a loop to process each error
- Styling input fields with errors
- Displaying error messages to the user
- Returning early from the function if there are any errors
This has made the user experience more intuitive and user-friendly, as users can now see all the errors they need to correct in one place.
Next steps
Section titled βNext stepsβIn the next step, we will calculate the total cost of the userβs tuition based on the level and number of hours they have selected.
But first, letβs update our assignment to include the changes we have made in this guide.