@practical-computer/error-handling

2.0.0 • Public • Published

Practical Error Handling

Error message handling that covers 99% of cases, through a mixture of restraint, extendability, and focus. It's also designed to make your error handling accessible by default.

Example

Here's a basic example, adapted from the demo page:

import {ErrorHandlingElement} from "@practical-computer/error-handling/error-handling-element"
import {clearErrorListsInForm} from "@practical-computer/error-handling/rendering"
import {applyErrorMappingToForm} from "@practical-computer/error-handling/error-mapping"

// Defining my own custom element to wrap the form, to prevent submits and
// clear out the error lists in the form to indicate it was successful.
class CustomErrorHandleElement extends ErrorHandlingElement {
  connectedCallback() {
    super.connectedCallback()

    this.form.addEventListener(`submit`, this.handleFormSubmit)
  }

  handleFormSubmit(event){
    if(!event.defaultPrevented){
      event.preventDefault()
      clearErrorListsInForm(this.form)
      console.debug(event)
      document.getElementById(`easy-console`).value += "Submit successful\n"
    }
  }
}

if (!window.customElements.get('custom-error-handling')) {
  window.customElements.define('custom-error-handling', CustomErrorHandleElement);
}

// An example error map, which you could receive as the JSON response from a fetch
// request
const errors = [
  {
    container_id: "email-field-errors",
    element_to_invalidate_id: "email-field",
    message: "This email has been taken",
    type: "taken"
  },
  {
    container_id: "email-field-errors",
    element_to_invalidate_id: "email-field",
    message: "Email is too short (pulled from server messages)",
    type: "tooShort"
  },
  {
    container_id: "test-form-error-container",
    element_to_invalidate_id: "test-form",
    message: "General errors from the server-side",
    type: "general"
  },
  {
    container_id: "random-missing-field-errors",
    element_to_invalidate_id: "random-missing-field",
    message: "Error that will end up in the fallback section",
    type: "api_error"
  },
]

const applyErrorMappingButton = document.getElementById(`apply-error-mapping`)
const form = document.getElementById(`test-form`)

applyErrorMappingButton.addEventListener(`click`, (event) => {
  applyErrorMappingToForm(form, errors)
  document.getElementById(`easy-console`).value += "Applied error map\n"
})
<body>
  <h1>Practical Error Handling Demo</h1>

  <custom-error-handling>
    <form id="test-form" aria-describedby='test-form-error-container'>
      <input type="email" id="email-field" aria-describedby="email-field-errors" minlength="4">
      <section id="email-field-errors" data-pf-error-container>
        <ul>
          <li data-pf-error-visible data-pf-error-type="error_1">ad-hoc error message 1 <small>(rendered on initial load)</small></li>
          <li data-pf-error-preserve data-pf-error-type="tooShort">Custom Preserved Error: Must be at least 4 characters<small>(rendered on initial load)</small></li>
        </ul>
      </section>

      <section id="test-form-error-container" data-pf-error-container>
        <h2>Form Errors</h2>
        <ul>
          <li data-pf-error-visible data-pf-error-type="error_2">ad-hoc error message 2 <small>(rendered on initial load)</small></li>
          <li data-pf-error-visible data-pf-error-preserve data-pf-error-type="error_3">Preserved Error <small>(rendered on initial load)</small></li>
        </ul>
      </section>

      <button type="submit">Submit</button>
    </form>
  </custom-error-handling>

  <p>
    <button id="apply-error-mapping">Apply error mapping</button>
  </p>

  <textarea id="easy-console" readonly rows=20></textarea>

  <template id="pf-error-list-item-template">
    <li><span>‼️</span> <span data-pf-error-message></span></li>
  </template>
</body>

Installation

npm install @practical-computer/error-handling

Check package.json for the list of exports.

Include the utility CSS for hiding/showing messages, or copy/paste into your codebase:

@import '@practical-computer/error-handling/css/util.css'

Quick Guide

Terms

The following terms are used for this package:

  • Input: The field (native or custom!) that you want to render errors for and reflect its validation state.
  • Form: The form itself!
  • Error list: An ul of the errors that apply for a particular input or Form.
    • Likewise, you have error list items, which are the items in these lists.
  • Error container: The element that contains the error list, either for a particular input or the overall form.
  • error mapping: An array of JSON objects that describe the entire validation state of the current form. You'd often return them as the response from a fetch request (but you don't have to!)

Data Attributes

The following data attributes are used by this package:

  • Inputs
    • Validation Event Handling flags. This package does not provide the validations, but does provide helper functions for checks if an event handler should proceed by checking the value of data-pf-validation:
      • data-pf-validation="input": This input should use input validation
      • data-pf-validation="change": This input should use change validation
      • data-pf-validation="focusout": This input should use focusout validation
      • data-pf-validation="skip": This input should skip any validations
    • data-pf-initial-load-errors: If present, this input has initial load errors, which should not be cleared out when reflecting the constraint validation for the initial load.
  • data-pf-error-container: Marks an element as an error container
  • Error list items
    • data-pf-error-visible: Marks that the error list item should be visible
    • data-pf-error-type: The error type for this list item (eg: tooShort, or some_custom_error_type)
    • data-pf-error-preserve: This error list item should not be removed when clearing the error list; useful for rendering custom error messages for default error types. Just because an error is preserved does not mean it is visible
  • data-pf-error-message Used in the error list item template element to indicate where an error message should be rendered in the markup as the textContent

Marking an input as invalid

We use aria-invalid="true" to indicate that an input is invalid. This makes your markup accessible by default while not requiring you to tack on a bunch of extra attributes.

Linking an input or form to an error container

To link an input or form to an error container, you:

  • Add the error container's markup to the page, with a unique id and the data-pf-error-container attribute
  • Include that id in the input/form's aria-describedby

We use the aria-describedby so that your markup is accessible by default. Since there can be multiple elements that describe an element, You specify which one is the error container using data-pf-error-container.

Example

<form id="test-form" aria-describedby='test-form-error-container'>
  <input type="email" id="email-field" aria-describedby="email-field-errors" minlength="4">
  <section id="email-field-errors" data-pf-error-container>
    <ul></ul>
  </section>

  <section id="test-form-error-container" data-pf-error-container>
    <h2>Form Errors</h2>
    <ul></ul>
  </section>

  <button type="submit">Submit</button>
</form>

Extending the custom elements

Since each application has different needs, the best way to utilize the custom elements that wrap around a form to provide event handlers is to subclass them to make your own versions:

import { applyErrorMappingFromResponse } from '@practical-computer/error-handling/request-processing'

class AppErrorHandlingElement extends ErrorHandlingElement {
  connectedCallback() {
    super.connectedCallback()

    this.addEventListener(`custom-fetch-library:request-error`, applyErrorMappingFromResponse)

    this.form.addEventListener(`submit`, (event) => {
      if(!event.defaultPrevented){
        event.preventDefault()
        this.makeCustomFetchRequest(...)
      }
    })
  }
}

if (!window.customElements.get('app-error-handling')) {
  window.customElements.define('app-error-handling', ApplicationErrorHandlingElement);
}

Handling the initial page load

  • Render any error list items that should be visible on the initial page load with the data-pf-error-visible attribute
  • Mark the invalid inputs with:
    • aria-invalid=true
    • data-pf-initial-load-errors

To reflect any other errors that might be present from the Constraint Validation API, you can use the reflectConstraintValidationForInitialLoad method in @practical-computer/error-handling/element-utilities.

This method will skip any form.elements with blank values, or the data-pf-initial-load-errors attribute.

Server-side errors

Since the server is the source of truth for your application, you should heavily rely on it for validations. Make reuqests against the server to validate input, and have the form return a response with the status code of 422 Unprocessable Entity and an error mapping JSON object (specified below).

The provided request processing functions expect a 422 response to indicate that the request has validation errors. It's an error, and the response's status code should reflect that.

Using the basic request processing function

import { applyErrorMappingFromResponse } from '@practical-computer/error-handling/request-processing'

const response = fetch(...)
const form = // ...

applyErrorMappingFromResponse(form, response)

Loading the Mrujs plugin

The Mrujs plugin is a function that can be used as an event handler for the ajax:response:error event:

import { unprocessableEntityResponseHandler } from '@practical-computer/error-handling/plugins/mrujs'
this.form.addEventListener('ajax:response:error', unprocessableEntityResponseHandler)

Error mapping specification

When returning error mappings, they need to have the following format:

// An array, since this is the collection of errors for this pass of data entry
[
  {
    "container_id": "dom-id-without-#", // the ID of the error container you want to append this error message to
    "element_to_invalidate_id": "dom-id-without-#", // the ID of the input that should be marked as invalid
    "message": "The error message to render", // The plaintext message used for the error
    "html_content": "<strong>Rich</strong> Markup", // an OPTIONAL HTML string that will be parsed using `DOMParser` and used as the message. If present, `message` is ignored
    "type": "error-type-key", //The identifier for what type of error this is
  },
  // ...
]

Why do the error containers need a list?

Error containers need a list because there can be multiple error messages that are relevant for this pass of data entry.

A single message is a list of one item! And we have so much power to style elements using CSS, that leaning into the semantics of ul is the way to go here. Plus, it helps make the error messages more accessible.

Design Overview

The Practical Framework's approach to error handling has the following core tenets:

  • Late validation is almost always better, since you don't want to bother someone until they're actually ready to submit data
  • Minimal client-side error handling makes the most sense, because client-side validations should never be trusted on their own. Therefore, it's easier & less bug-prone to simply submit a request to the server, and have the server return any errors (since it's the source of truth in the end)
  • If there are multiple errors, render them all under the field they're related to. If there's no specific field, render them at the end of the form.

Reviewing the source

One of the best ways to understand this package is to glance at the source. It's a remarkably small library, with the ESM modules broken up by purpose.

What @practical-computer/error-handling does

  • Provides functions for retrieving:
    • The error container
    • Error list
    • Any preserved errors for a list (or for a specific type of error)
  • Rendering helper functions, including:
    • Reflecting the current ValidityState of an element
    • Applyig the data-pf and ARIA attributes to mark an element as invalid, without going through the Constraint Validation API
    • Marking an error type as visible for an element
    • Creating a new error list item element
    • Clearing the error list for an element, or for an entire form.
  • Premade event handlers to validate:
    • submit events
    • live input validation
    • focusout validation
  • Prebuilt request processing functions, including a "plugin" event handler for Mrujs
  • A basic ErrorHandlingElement that you can subclass to make custom elements specific to your application
    • It can also be included as is by importing the pf-error-handling element
  • The foundations to support fieldset validations, useful for cases like "at least one value must be checked."

Fully extendable

The package exports all of its functions so that you can directly use them in your application logic. And because it does not provide any custom validation logic you need to fight against, and relies on the Constraint Validation API, you have the entire toolbox of Javascript available to you.

The custom elements that are exported are also designed to be subclassed, because each application has a different networking stack and needs.

What @practical-computer/error-handling does not do

Importantly, this package does not include the following by design:

Validation Logic

Validation is almost always application/framework dependent; and best served by you writing the validation logic.

Markup for rendering errors

You provide the markup, using ARIA attributes, a small set of data- attributes (prefixed with data-pf), and a template element. Fitting error messages into a design is tough and extremely context specific. We trust your judgement, and render the text and set the data-pf-error-visible attributes in the places you put us to.

Automatic integrations/hooks/event handlers

Again, we don't know the full story about your frontend, so we're not going to make any assumptions. You import the things you need, extend the custom elements we provide, and have full control over where and how error handling occurs.


Only you know the needs of your application, so @practical-computer/error-handling defers to your expertise and stays in its lane.

Progressively enhances

The Practical Error Handling approach allows you to render error messages through all variations of "ready":

  • You can render error messages server-side as part of the page load or standard form submission, and they'll show up without any JS (thanks to the power of CSS!)
  • When the app's JS fails, the basic attributes from the Constraint Validation API can take over.
  • Since it only uses the standard event handlers, and only triggers when you've explictly event handlers on the form/input, you can:
    • Add any custom logic that's needed
    • Seamlessly support fetch, AJAX, or whatever request mechanism you want to use

Fieldset validation

This is a more advanced topic (that IMO can be handled better by server-side validations), but the package does ship with the foundations for validating an entire fieldset.

I would recommend reading Part 3 of the Cloud Four guide for the context and explanation of this pattern, then check out:

Acknowledgements

This package is heavily inspired by the work done by Gerardo Rodriguez for Cloud Four. You can read the original 4-part guide here: https://cloudfour.com/thinks/progressively-enhanced-form-validation-part-1-html-and-css/

Readme

Keywords

Package Sidebar

Install

npm i @practical-computer/error-handling

Weekly Downloads

0

Version

2.0.0

License

MIT

Unpacked Size

139 kB

Total Files

37

Last publish

Collaborators

  • tcannonfodder