Form validation

When a user enters a form with invalid inputs, such as an invalid email address, then we want our UI to tell the user what went wrong.

We can pass information about invalid inputs to the frontend by using return someValue:

// SignUpForm.telefunc.ts
// Environment: server
 
export async function onFormSubmit(email: string, password: string) {
  // Form validation
  const inputErrors = {}
 
  if (!email) {
    inputErrors.email = 'Please enter your email.'
  } else if (!email.includes('@')) {
    inputErrors.email = 'Invalid email; make sure to enter a valid email.'
  }
 
  if (!password) {
    inputErrors.password = 'Please enter a password.'
  } else if (password.length < 8) {
    inputErrors.password = 'Password must have at least 8 charachters.'
  }
 
  if (Object.keys(inputErrors).length > 0) {
    // Instead of `throw Abort()`
    return { inputErrors }
  }
 
  // Some ORM/SQL query
  const user = await User.create({ email, password })
 
  return { user }
}

throw Abort(someValue)

We can use throw Abort(someValue) instead of return someValue:

// SignUpForm.telefunc.ts
// Environment: server
 
import { Abort } from 'telefunc'
 
export async function onFormSubmit(email: string, password: string) {
  if (!email) {
    throw Abort({
      inputErrors: {
        email: 'Please enter your email.'
      }
    })
  }
  /* ... */
}

In general, we recommend using return { someValue } instead of throw Abort(someValue) because:

  1. It makes the code's intent clearer: the reader of our code knows that return someValue is an expected case that will be handled by the frontend, whereas when throwing an error it isn't obvious whether/where/how the error will be handled by the frontend.
  2. return someValue has better TypeScript DX, as TypeScript doesn't typecheck catched errors:
    try {
      /* ... */
    } catch(err: unkown) {
      // err needs to be casted
      const errCasted = (err as ValidationError | SomeOtherError)
    }