diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py
index 1a42746..0db2a12 100644
--- a/AutoscoperM/AutoscoperM.py
+++ b/AutoscoperM/AutoscoperM.py
@@ -18,7 +18,7 @@
 )
 from slicer.util import VTKObservationMixin
 
-from AutoscoperMLib import IO, SubVolumeExtraction
+from AutoscoperMLib import IO, SubVolumeExtraction, Validation
 
 #
 # AutoscoperM
@@ -447,7 +447,8 @@ def onGeneratePartialVolumes(self):
             trackingSubDir = self.ui.trackingSubDir.text
             modelSubDir = self.ui.modelSubDir.text
             segmentationNode = self.ui.pv_SegNodeComboBox.currentNode()
-            if not self.logic.validateInputs(
+
+            Validation.validateInputs(
                 volumeNode=volumeNode,
                 segmentationNode=segmentationNode,
                 mainOutputDir=mainOutputDir,
@@ -455,9 +456,7 @@ def onGeneratePartialVolumes(self):
                 transformSubDir=tfmSubDir,
                 trackingSubDir=trackingSubDir,
                 modelSubDir=modelSubDir,
-            ):
-                raise ValueError("Invalid inputs")
-                return
+            )
 
             self.logic.createPathsIfNotExists(
                 mainOutputDir,
@@ -503,7 +502,7 @@ def onGenerateConfig(self):
             camCalList = self.ui.camCalList
 
             # Validate the inputs
-            if not self.logic.validateInputs(
+            Validation.validateInputs(
                 volumeNode=volumeNode,
                 mainOutputDir=mainOutputDir,
                 configFileName=configFileName,
@@ -513,17 +512,13 @@ def onGenerateConfig(self):
                 trialList=trialList,
                 partialVolumeList=partialVolumeList,
                 camCalList=camCalList,
-            ):
-                raise ValueError("Invalid inputs")
-                return
-            if not self.logic.validatePaths(
+            )
+            Validation.validatePaths(
                 mainOutputDir=mainOutputDir,
                 tiffDir=os.path.join(mainOutputDir, tiffSubDir),
                 radiographSubDir=os.path.join(mainOutputDir, radiographSubDir),
                 calibDir=os.path.join(mainOutputDir, calibrationSubDir),
-            ):
-                raise ValueError("Invalid paths")
-                return
+            )
 
             def get_staged_items(listWidget):
                 staged_items = []
@@ -600,8 +595,8 @@ def get_checked_items(listWidget):
                 self.ui.voxelSizeZ.value,
             ]
 
-            # Validate the inputs
-            if not self.logic.validateInputs(
+            # Validate the extracted parameters
+            Validation.validateInputs(
                 *trialDirs,
                 *partialVolumeFiles,
                 *camCalFiles,
@@ -609,8 +604,7 @@ def get_checked_items(listWidget):
                 *volumeFlip,
                 *renderResolution,
                 *voxel_spacing,
-            ):
-                raise ValueError("Invalid inputs")
+            )
 
             # generate the config file
             IO.generateConfigFile(
@@ -639,15 +633,13 @@ def onImportModels(self):
 
             volumeNode = self.ui.volumeSelector.currentNode()
 
-            if not self.logic.validateInputs(voluemNode=volumeNode):
-                raise ValueError("Invalid inputs")
-                return
+            Validation.validateInputs(volumeNode=volumeNode)
 
             if self.ui.segSTL_loadRadioButton.isChecked():
                 segmentationFileDir = self.ui.segSTL_modelsDir.currentPath
-                if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir):
-                    raise ValueError("Invalid paths")
-                    return
+
+                Validation.validatePaths(segmentationFileDir=segmentationFileDir)
+
                 segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*"))
                 segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
                 segmentationNode.CreateDefaultDisplayNodes()
@@ -660,7 +652,7 @@ def onImportModels(self):
                         slicer.mrmlScene.RemoveNode(returnedNode)
                     self.ui.progressBar.setValue((idx + 1) / len(segmentationFiles) * 100)
             else:  # Should never happen but just in case
-                raise Exception("Please select the 'Segmentation From Model' option in order to import models")
+                raise ValueError("Please select the 'Segmentation From Model' option in order to import models")
                 return
         slicer.util.messageBox("Success!")
 
@@ -674,9 +666,7 @@ def onSegmentation(self):
 
             volumeNode = self.ui.volumeSelector.currentNode()
 
-            if not self.logic.validateInputs(voluemNode=volumeNode):
-                raise ValueError("Invalid inputs")
-                return
+            Validation.validateInputs(volumeNode=volumeNode)
 
             if self.ui.segGen_autoRadioButton.isChecked():
                 currentVolumeNode = volumeNode
@@ -702,7 +692,7 @@ def onSegmentation(self):
                         slicer.mrmlScene.RemoveNode(segmentationNode)
                         currentVolumeNode = self.logic.getNextItemInSequence(volumeNode)
             else:  # Should never happen but just in case
-                raise Exception("Please select the 'Automatic Segmentation' option in order to generate segmentations")
+                raise ValueError("Please select the 'Automatic Segmentation' option in order to generate segmentations")
                 return
         slicer.util.messageBox("Success!")
 
@@ -725,7 +715,7 @@ def onLoadPV(self):
             tfms_scale = glob.glob(os.path.join(mainOutputDir, transformSubDir, "*_scale.tfm"))
 
             if len(vols) == 0:
-                raise Exception("No data found")
+                raise ValueError("No data found")
                 return
 
             if len(vols) != len(tfms_t) != len(tfms_scale):
@@ -801,18 +791,14 @@ def populateListFromOutputSubDir(self, listWidget, fileSubDir, itemType="file"):
         listWidget.clear()
 
         mainOutputDir = self.ui.mainOutputSelector.currentPath
-        if not self.logic.validateInputs(
+        Validation.validateInputs(
             listWidget=listWidget,
             mainOutputDir=mainOutputDir,
             fileSubDir=fileSubDir,
-        ):
-            raise ValueError("Invalid inputs")
-            return
+        )
 
         fileDir = os.path.join(mainOutputDir, fileSubDir)
-        if not self.logic.validatePaths(fileDir=fileDir):
-            raise ValueError(f"Invalid input: subdirectory '{fileDir}' does not exist.")
-            return
+        Validation.validatePaths(fileDir=fileDir)
 
         if itemType == "file":
             listFiles = [f.name for f in os.scandir(fileDir) if os.path.isfile(f)]
@@ -1181,66 +1167,6 @@ def showVolumeIn3D(volumeNode: slicer.vtkMRMLVolumeNode):
         logic.UpdateDisplayNodeFromVolumeNode(displayNode, volumeNode)
         slicer.mrmlScene.RemoveNode(slicer.util.getNode("Volume rendering ROI"))
 
-    @staticmethod
-    def validateInputs(*args: tuple, **kwargs: dict) -> bool:
-        """
-        Validates that the provided inputs are not None.
-
-        :param args: list of inputs to validate
-        :param kwargs: list of inputs to validate
-
-        :return: True if all inputs are valid, False otherwise
-        """
-        statuses = []
-        for arg in args:
-            status = True
-            if arg is None:
-                logging.error(f"{arg} is None")
-                status = False
-            if isinstance(arg, str) and arg == "":
-                logging.error(f"{arg} is an empty string")
-                status = False
-            statuses.append(status)
-
-        for name, arg in kwargs.items():
-            status = True
-            if arg is None:
-                logging.error(f"{name} is None")
-                status = False
-            if isinstance(arg, str) and arg == "":
-                logging.error(f"{name} is an empty string")
-                status = False
-            statuses.append(status)
-
-        return all(statuses)
-
-    @staticmethod
-    def validatePaths(*args: tuple, **kwargs: dict) -> bool:
-        """
-        Checks that the provided paths exist.
-
-        :param args: list of paths to validate
-        :param kwargs: list of paths to validate
-
-        :return: True if all paths exist, False otherwise
-        """
-        statuses = []
-        for arg in args:
-            status = True
-            if not os.path.exists(arg):
-                logging.error(f"{arg} does not exist")
-                status = False
-            statuses.append(status)
-
-        for name, path in kwargs.items():
-            status = True
-            if not os.path.exists(path):
-                logging.error(f"{name} ({path}) does not exist")
-                status = False
-            statuses.append(status)
-
-        return all(statuses)
-
     @staticmethod
     def createPathsIfNotExists(*args: tuple) -> None:
         """
diff --git a/AutoscoperM/AutoscoperMLib/Validation.py b/AutoscoperM/AutoscoperMLib/Validation.py
new file mode 100644
index 0000000..1de2a35
--- /dev/null
+++ b/AutoscoperM/AutoscoperMLib/Validation.py
@@ -0,0 +1,64 @@
+import os
+
+
+class ValueErrorsException(Exception):
+    """A custom exception class that accepts a list of errors."""
+
+    def __init__(self, errors):
+        if not isinstance(errors, list):
+            raise ValueError("The errors input must be a list")
+        if len(errors) < 1:
+            raise ValueError("The errors list must contain at least one error")
+        self.errors = errors
+        super().__init__("\n".join(errors))
+
+    def __str__(self):
+        err_str = "Invalid value{}".format("" if len(self.errors) == 0 else "s")
+
+        return err_str + ":\n" + "\n".join(self.errors)
+
+
+def validateInputs(*args: tuple, **kwargs: dict) -> None:
+    """
+    Validates that the provided inputs are not None or empty.
+
+    :param args: list of inputs to validate
+    :param kwargs: dictionary of inputs to validate
+    :raises: ValueErrorsException
+    """
+    errors = []
+    for arg in args:
+        if arg is None:
+            errors.append("Input argument is None")
+        if isinstance(arg, str) and arg == "":
+            errors.append("Input argument is an empty string")
+
+    for name, arg in kwargs.items():
+        if arg is None:
+            errors.append(f"Input '{name}' is None")
+        if isinstance(arg, str) and arg == "":
+            errors.append(f"Input '{name}' is an empty string")
+
+    if len(errors) > 0:
+        raise ValueErrorsException(errors)
+
+
+def validatePaths(*args: tuple, **kwargs: dict) -> None:
+    """
+    Checks that the provided paths exist.
+
+    :param args: list of paths to validate
+    :param kwargs: list of paths to validate
+    :raises: ValueErrorsException
+    """
+    errors = []
+    for arg in args:
+        if not os.path.exists(arg):
+            errors.append(f"Input path '{arg}' does not exist")
+
+    for name, path in kwargs.items():
+        if not os.path.exists(path):
+            errors.append(f"Input path '{name}' ({path}) does not exist")
+
+    if len(errors) > 0:
+        raise ValueErrorsException(errors)
diff --git a/AutoscoperM/CMakeLists.txt b/AutoscoperM/CMakeLists.txt
index f752650..f84983a 100644
--- a/AutoscoperM/CMakeLists.txt
+++ b/AutoscoperM/CMakeLists.txt
@@ -7,6 +7,7 @@ set(MODULE_PYTHON_SCRIPTS
   ${MODULE_NAME}Lib/__init__.py
   ${MODULE_NAME}Lib/IO.py
   ${MODULE_NAME}Lib/SubVolumeExtraction.py
+  ${MODULE_NAME}Lib/Validation.py
   )
 
 set(MODULE_PYTHON_RESOURCES