diff --git a/bin/slurm_calibrate.sh b/bin/slurm_calibrate.sh
index 5786f7ce5737ed6a3be7a2d33658a1e359d5d331..98c45daedcaf5631efc55fc3ffd2c31f7d6c9143 100755
--- a/bin/slurm_calibrate.sh
+++ b/bin/slurm_calibrate.sh
@@ -26,6 +26,9 @@ echo "hostname: $(hostname)"
 
 export CAL_NOTEBOOK_NAME="$notebook"
 
+# This is created by calibrate.py in CAL_WORKING_DIR
+setup_logger="setup-logging-nb.py"
+
 # make sure we use agg backend
 export MPLBACKEND=AGG
 
@@ -40,13 +43,12 @@ then
     sleep 15
 fi
 
-echo "Running notebook"
 if [ "$caltype" == "CORRECT" ]
 then
-  # calparrot stores and repeats calcat queries
-  ${python_path} -m calparrot -- ${python_path} -m princess ${nb_path} --save
+    # calparrot stores and repeats calcat queries
+    ${python_path} -m calparrot -- ${python_path} -m princess ${nb_path} --save --run-before "$setup_logger"
 else
-  ${python_path} -m princess ${nb_path} --save
+    ${python_path} -m princess ${nb_path} --save --run-before "$setup_logger"
 fi
 
 # stop the cluster if requested
diff --git a/notebooks/test/test-logging.ipynb b/notebooks/test/test-logging.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..f09b1109e399582ea24e76c9a22d05a0c9873a93
--- /dev/null
+++ b/notebooks/test/test-logging.ipynb
@@ -0,0 +1,125 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "98e38fec",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "in_folder = \"./\" # input folder\n",
+    "out_folder = \"./\" # output folder"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "7fbce574",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import logging\n",
+    "import warnings\n",
+    "\n",
+    "from cal_tools.warnings import CalibrationWarning"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "1bf72c57",
+   "metadata": {},
+   "source": [
+    "## WARNINGS"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c0d290a1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class TestCalWarning(CalibrationWarning):\n",
+    "    \"\"\"Base class for custom user warnings\"\"\"\n",
+    "    pass\n",
+    "\n",
+    "\n",
+    "if 1 < 2:\n",
+    "    warnings.warn('This inequality is true!', TestCalWarning)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "6a4d4f01",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "warnings.warn('This user warning will be ignored', UserWarning)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "9b79ea47",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# This wont be logged\n",
+    "logging.warning(\"This is a warning message using logging standard library. It wont be logged\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "2b22e2e0",
+   "metadata": {},
+   "source": [
+    "## ERRORS"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0f3bfeb7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# This wont be logged\n",
+    "logging.error(\"Logging some (ERROR) without failing the notebook. It wont be logged\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c3b87719",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from cal_tools.exceptions import CalibrationError\n",
+    "\n",
+    "raise CalibrationError('Testing Calibration Failure!')"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": ".venv",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/src/cal_tools/exceptions.py b/src/cal_tools/exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..5dbcbca65f8d7a81a4193df6821ca041d2df53ac
--- /dev/null
+++ b/src/cal_tools/exceptions.py
@@ -0,0 +1,2 @@
+class CalibrationError(Exception):
+    pass
diff --git a/src/cal_tools/warnings.py b/src/cal_tools/warnings.py
new file mode 100644
index 0000000000000000000000000000000000000000..d54686ba0fe5fc61e33d14df1b81b4bd4bb52668
--- /dev/null
+++ b/src/cal_tools/warnings.py
@@ -0,0 +1,2 @@
+class CalibrationWarning(UserWarning):
+    pass
diff --git a/src/xfel_calibrate/calibrate.py b/src/xfel_calibrate/calibrate.py
index 5d2b63c6f4d00a2e717bd86cd95c37e5b56b2a0c..61f35e0a85fcd83b313d237ebbb4602d0efec48a 100755
--- a/src/xfel_calibrate/calibrate.py
+++ b/src/xfel_calibrate/calibrate.py
@@ -675,6 +675,12 @@ def run(argv=None):
         cal_work_dir / "pycalib-run-nb.sh"
     )
 
+    # Copy the setup logger handler
+    shutil.copy2(
+        os.path.join(PKG_DIR, "setup_logging.py"),
+        cal_work_dir / "setup-logging-nb.py"
+    )
+
     if nb_details.user_venv:
         print("Using specified venv:", nb_details.user_venv)
         python_exe = str(nb_details.user_venv / 'bin' / 'python')
diff --git a/src/xfel_calibrate/notebooks.py b/src/xfel_calibrate/notebooks.py
index 5ac28ac12340e67410fe62750fd86e95ac8b8efd..97769638974ee8bd60545b4f70699c0cd979bbf4 100644
--- a/src/xfel_calibrate/notebooks.py
+++ b/src/xfel_calibrate/notebooks.py
@@ -360,6 +360,14 @@ notebooks = {
                 "cluster cores": 1
             },
         },
+        "TEST-LOGGING": {
+            "notebook": "notebooks/test/test-logging.ipynb",
+            "concurrency": {
+                "parameter": None,
+                "default concurrency": None,
+                "cluster cores": 1,
+            },
+        },
     },
     "TEST-RAISES-ERRORS": {
         "TEST-BAD-KEY": {
diff --git a/src/xfel_calibrate/setup_logging.py b/src/xfel_calibrate/setup_logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9237517c50870e9d492672939d1799611004256
--- /dev/null
+++ b/src/xfel_calibrate/setup_logging.py
@@ -0,0 +1,94 @@
+import json
+import os
+import sys
+import warnings
+from datetime import datetime
+from typing import Any, Type
+
+import IPython
+
+from cal_tools.warnings import CalibrationWarning
+
+JOB_ID = os.getenv('SLURM_JOB_ID', 'local')
+
+
+def get_class_hierarchy(cls: Type) -> str:
+    """Get the full class hierarchy of a class."""
+    return ".".join(c.__name__ for c in cls.__mro__ if c != object)
+
+
+def get_log_info(
+    message: str,
+    log_type: Type[Exception] | Type[Warning]
+) -> dict:
+    """Create a dictionary with log information."""
+    return {
+        "timestamp": datetime.now().isoformat(),
+        "message": message,
+        "class": get_class_hierarchy(log_type),
+    }
+
+
+def log_error(
+    exc_type: Type[Exception],
+    exc_value: Exception,
+    exc_traceback: Any
+) -> None:
+    """Log error information to file."""
+    try:
+        error_info = get_log_info(str(exc_value), exc_type)
+
+        with open(f"errors_{JOB_ID}.log", "a") as log_file:
+            log_file.write(json.dumps(error_info) + "\n")
+
+    except Exception as e:
+        sys.stdout.write(f"Logging failed: {e}\n")
+
+
+original_showwarning = warnings.showwarning
+
+
+def handle_warning(
+    message: Warning, category: Type[Warning],
+    # these are needed for `warnings.showwarning`
+    filename=None, lineno=None,
+    file=None, line=None,
+) -> None:
+    """Log and display warnings."""
+    try:
+        if issubclass(category, CalibrationWarning):
+            warning_info = get_log_info(str(message), category)
+
+            with open(f"warnings_{JOB_ID}.log", "a") as log_file:
+                log_file.write(json.dumps(warning_info) + "\n")
+
+    except Exception as e:
+        sys.stderr.write(f"Warning logging failed: {e}\n")
+    finally:
+        # Ensure warning is displayed in notebook.
+        original_showwarning(message, category, filename, lineno, file, line)
+
+
+# Get IPython shell
+shell = IPython.get_ipython()
+
+# Store original handlers
+original_showtraceback = shell.showtraceback
+
+
+# Custom exception handler for IPython
+def custom_showtraceback(*args, **kwargs):
+    etype, value, tb = sys.exc_info()
+    log_error(etype, value, tb)
+    original_showtraceback(*args, **kwargs)
+    if not args and not kwargs:
+        return etype, value, tb
+
+
+# Install handlers
+shell.showtraceback = custom_showtraceback
+warnings.showwarning = handle_warning
+
+# Ignore all warnings globally, except CalibrationWarning
+warnings.filterwarnings("ignore")
+warnings.simplefilter("default", CalibrationWarning)