DEV Community

Raúl R
Raúl R

Posted on • Updated on

Validations and error handling on Rails and React

Today, I’ll be focusing on error and exception handling in a Rails application.

As a quick refresher, MVC architecture follows the Rails concepts of convention over configuration and separation of concerns.

  • Model: will contain validations, database relationships, callbacks, and any custom logic.
  • Controller: will have methods to manage data flow for the models, including the full set of CRUD features.
  • Views: will contain a corresponding view for each of the pages that the end user will access.

For the purposes of this topic, I will focus on the controller. For example, a user creates a new task associated with their account:

tasks_controller.rb

def create
        task = Task.create(task_params)
        render json: task, status: :created
    end
Enter fullscreen mode Exit fullscreen mode

The request would be handled by the controller, which will use the task model to create and save new task instance to the db, then rendering the response (in this case, the task object itself) as json, along with a status of "ok".

What happens when a CRUD action fails? Well, we need to determine why a request might fail and most importantly, get that information to the front-end so that the end-user knows what needs to be done to address the issue.

Active Record validations

Whenever creating or updating data in a db, it's imperative to include strong params–explicitly specifying which attributes can be edited. Why? To prevent a user from being able to modify data they should not normally be allowed to access such as password, permissions, etc.

Let's go back to our "tasks" controller. In order to create or update a task, we want to make sure certain data is present. First, we want to make sure we've established the required attributes. For instance:

tasks_controller.rb

def create
        task = Task.create(task_params)
        render json: task, status: :created
    end

private

    def task_params 
        params.permit(:name, :description, :priority, :project_id, :user_id, :status)
    end
Enter fullscreen mode Exit fullscreen mode

The private method task_params establishes which attributes can be edited on a given task. How can we better control the integrity of the data submitted when creating or updating an object? In come our Active Record validations!

Validations are method calls that live at the top of model class definitions and prevent an instance of that model from being saved to the database if the data doesn't meet certain requirements (established by us as developers).

Active Record validations will only be checked when adding or updating data through Ruby/Rails. We can also call the #valid? method if we want to trigger a validation before data touches the db. Let's take a look at the "task" model:

task.rb

class Task < ApplicationRecord
    validates :name, presence: true
    validates :description, presence: true
    validates :priority, presence: true
    validates :project_id, presence: true
    belongs_to :user
    belongs_to :project
end
Enter fullscreen mode Exit fullscreen mode

We can use the "validates" method, then specify the attribute being validated, and the type of validation. The validations are saying "in order for a task to be valid and saved or updated in the db, it needs to have a name, description, priority, and project_id". We can add all sorts of validations, but for now, let's keep it simple. How would an end-user know if the data they are submitting isn't valid? Well, we need to raise an exception and show them an error.

tasks_controller.rb

def create
  task = Task.create!(task_params)
  render json: task, status: :created
rescue ActiveRecord::RecordInvalid => invalid
  render json: { errors: invalid.record.errors }, status: :unprocessable_entity
end

private

    def task_params 
        params.permit(:name, :description, :priority, :project_id, :user_id, :status)
    end
Enter fullscreen mode Exit fullscreen mode

Adding a bang or exclamation mark after the method checks the validity of the data AND raises an exception if it isn't valid. So, we have two possible outcomes:

  • If the data is valid, the instance is created and saved to the db. Great!
  • If it isn't valid, then we get an exception from Active Record, which we can drill into with the record method to retrieve its errors.

Going one step further, we can use the full_messages method to access the full error messages, which will likely be more helpful to the end-user.

If you'd like to learn more about Active Record validations, I highly encourage you to check out this extensive resource which includes many different types of validations.

Front-end

Now that we know how to validate data and retrieve errors if the data is invalid, how do we show that to the end-user in the front-end? Well, one great option is to set a condition in our request, so that any errors can be set to React state and then rendered in the front-end. For instance:

const handleSubmit = (e) => {
        e.preventDefault();
        const newTask = {
          name: formData.name,
          description: formData.description,
          priority: formData.priority,
          project_id: formData.project,
          user_id: user.id,
          status: "new",
          };
        fetch("/tasks", {
          method: "POST",
          headers: {
            "Content-type": "application/json"
          },
          body: JSON.stringify(newTask)
        })
        .then((r) => {
          if (r.ok) {
            r.json()
            .then((newTask) => onAddTask(newTask))
            handleClose()
          } else {
            r.json().then((errorData) => setErrors(errorData.errors))
          }
        })
      };
Enter fullscreen mode Exit fullscreen mode

Let's say I try to create a task without a description. My validations require every task to have a description, so we would get an error, which could then get set to an "errors" state variable with the "setErrors" setter function, then rendered in the front-end via something like this:

{errors
  ? errors.map((error,index) => 
  <Typography 
    key={index} 
    sx={{ color: 'red' }}
  >
    {error}.
  </Typography>)
  : null
}
Enter fullscreen mode Exit fullscreen mode

This would look something like this on the front-end:

Image description

We can even go one step further and use React context to avoid prop drilling if we foresee the need to surface errors in many places across our application.

import React, { useState } from "react";

const ErrorContext = React.createContext();

const ErrorProvider = ({children}) => {
    const [errors, setErrors] = useState(null);

    return(
        <ErrorContext.Provider value={{ errors, setErrors }}>
            {children}
        </ErrorContext.Provider>
    );
};

export { ErrorContext, ErrorProvider };
Enter fullscreen mode Exit fullscreen mode

That way, we can ensure that it's easy to update the "errors" state variable and display it where needed!

Errors can be no fun, but they are necessary and helpful. As developers, we want to ensure a certain level of integrity in our data and that we're surfacing clear and helpful error messages that can guide end-users toward a solution. Thanks for reading!

Top comments (0)