Index: python-pyvista/doc/source/conf.py
===================================================================
--- python-pyvista.orig/doc/source/conf.py	2026-01-30 19:08:44.499329438 +0100
+++ python-pyvista/doc/source/conf.py	2026-01-30 19:08:44.493480646 +0100
@@ -262,6 +262,7 @@
     (r'py:.*', '.*Trimesh'),
     (r'py:.*', 'networkx.*'),
     (r'py:.*', 'Rotation'),
+    (r'py:.*', '.*VtkEvent'),
     (r'py:.*', 'vtk.*'),
     (r'py:.*', '_vtk.*'),
     (r'py:.*', 'VTK'),
Index: python-pyvista/pyvista/core/__init__.py
===================================================================
--- python-pyvista.orig/pyvista/core/__init__.py	2026-01-30 19:08:44.499329438 +0100
+++ python-pyvista/pyvista/core/__init__.py	2026-01-30 19:08:44.493756707 +0100
@@ -23,6 +23,8 @@
 from .errors import PyVistaEfficiencyWarning as PyVistaEfficiencyWarning
 from .errors import PyVistaFutureWarning as PyVistaFutureWarning
 from .errors import PyVistaPipelineError as PyVistaPipelineError
+from .errors import VTKExecutionError as VTKExecutionError
+from .errors import VTKExecutionWarning as VTKExecutionWarning
 from .errors import VTKVersionError as VTKVersionError
 from .filters import CompositeFilters as CompositeFilters
 from .filters import DataObjectFilters as DataObjectFilters
Index: python-pyvista/pyvista/core/errors.py
===================================================================
--- python-pyvista.orig/pyvista/core/errors.py	2026-01-30 19:08:44.499329438 +0100
+++ python-pyvista/pyvista/core/errors.py	2026-01-30 19:08:44.493909630 +0100
@@ -203,6 +203,26 @@
         super().__init__(message)
 
 
+class VTKExecutionError(RuntimeError):
+    """Exception when a VTK output message is detected.
+
+    .. versionadded:: 0.47
+
+    Parameters
+    ----------
+    message : str
+        Error message.
+
+    """
+
+    def __init__(
+        self,
+        message='VTK output message was detected by PyVista.',
+    ) -> None:  # numpydoc ignore=PR01,RT01
+        """Call the base class constructor with the custom message."""
+        super().__init__(message)
+
+
 class PyVistaDeprecationWarning(Warning):
     """Non-supressed Deprecation Warning."""
 
@@ -213,3 +233,11 @@
 
 class PyVistaEfficiencyWarning(Warning):
     """Efficiency warning."""
+
+
+class VTKExecutionWarning(RuntimeWarning):
+    """Warning when a VTK output message is detected.
+
+    .. versionadded:: 0.47
+
+    """
Index: python-pyvista/pyvista/core/utilities/observers.py
===================================================================
--- python-pyvista.orig/pyvista/core/utilities/observers.py	2026-01-30 19:08:44.499329438 +0100
+++ python-pyvista/pyvista/core/utilities/observers.py	2026-01-30 19:09:38.073642926 +0100
@@ -10,13 +10,20 @@
 import sys
 import threading
 import traceback
+from typing import TYPE_CHECKING
 from typing import NamedTuple
 
 import pyvista
 from pyvista._deprecate_positional_args import _deprecate_positional_args
+from pyvista._warn_external import warn_external
 from pyvista.core import _vtk_core as _vtk
+from pyvista.core.errors import VTKExecutionError
+from pyvista.core.errors import VTKExecutionWarning
 from pyvista.core.utilities.misc import _NoNewAttrMixin
 
+if TYPE_CHECKING:
+    from typing_extensions import Self
+
 
 def set_error_output_file(filename):
     """Set a file to write out the VTK errors.
@@ -51,46 +58,130 @@
     Parameters
     ----------
     raise_errors : bool, default: False
-        Raise a ``RuntimeError`` when a VTK error is encountered.
+        Raise a ``pyvista.VTKExecutionError`` (a runtime error) when a VTK error
+        is observed.
+
+        .. versionchanged:: 0.47
+
+            A ``pyvista.VTKExecutionError`` is now raised instead of a generic
+            ``RuntimeError``.
 
     send_to_logging : bool, default: True
         Determine whether VTK errors raised within the context should
         also be sent to logging.
 
+    emit_warnings : bool, default: False
+        Emit a ``pyvista.VTKExecutionWarning`` (a runtime warning) when a VTK warning
+        is observed.
+
+        .. versionadded:: 0.47
+
     Examples
     --------
-    Catch VTK errors using the context manager.
+    Catch VTK errors using the context manager. This only sends to
+    logging by default.
 
     >>> import pyvista as pv
     >>> with pv.VtkErrorCatcher() as error_catcher:
     ...     sphere = pv.Sphere()
 
+    Raise VTK errors as Python errors and emit VTK warnings as Python warnings.
+
+    >>> with pv.VtkErrorCatcher(
+    ...     raise_errors=True, emit_warnings=True
+    ... ) as error_catcher:
+    ...     sphere = pv.Sphere()
+
     """
 
     @_deprecate_positional_args
-    def __init__(self, raise_errors: bool = False, send_to_logging: bool = True) -> None:  # noqa: FBT001, FBT002
+    def __init__(
+        self,
+        raise_errors: bool = False,  # noqa: FBT001, FBT002
+        send_to_logging: bool = True,  # noqa: FBT001, FBT002
+        emit_warnings: bool = False,  # noqa: FBT001, FBT002
+    ) -> None:
         """Initialize context manager."""
         self.raise_errors = raise_errors
         self.send_to_logging = send_to_logging
+        self.emit_warnings = emit_warnings
 
-    def __enter__(self) -> None:
+    def __enter__(self: Self) -> Self:
         """Observe VTK string output window for errors."""
-        error_output = _vtk.vtkStringOutputWindow()
+        self._start_observing()
+        return self
+
+    def _start_observing(self):
+        output_window = _vtk.vtkStringOutputWindow()
         error_win = _vtk.vtkOutputWindow()
         self._error_output_orig = error_win.GetInstance()
-        error_win.SetInstance(error_output)
-        obs = Observer(log=self.send_to_logging, store_history=True)
-        obs.observe(error_output)
-        self._observer = obs
+        error_win.SetInstance(output_window)
+
+        obs = Observer(event_type='ErrorEvent', log=self.send_to_logging, store_history=True)
+        obs.observe(output_window)
+        self._error_observer = obs
+
+        obs = Observer(event_type='WarningEvent', log=self.send_to_logging, store_history=True)
+        obs.observe(output_window)
+        self._warning_observer = obs
 
     def __exit__(self, *args):
         """Stop observing VTK string output window."""
+        self._stop_observing()
+        self._emit_warnings_and_raise_errors()
+
+    def _stop_observing(self):
         error_win = _vtk.vtkOutputWindow()
         error_win.SetInstance(self._error_output_orig)
-        self.events = self._observer.event_history
-        if self.raise_errors and self.events:
-            errors = [RuntimeError(f'{e.kind}: {e.alert}', e.path, e.address) for e in self.events]
-            raise RuntimeError(errors)
+
+    def _emit_warnings_and_raise_errors(self):
+        if self.emit_warnings and self.warning_events:
+            self._emit_warning(self._runtime_warning_message)
+        if self.raise_errors and self.error_events:
+            self._raise_error(self._runtime_error_message)
+
+    @property
+    def events(self) -> list[VtkEvent]:  # numpydoc ignore=RT01
+        """List of all VTK warning and error events observed.
+
+        .. versionadded:: 0.47
+
+        """
+        return [*self._warning_observer.event_history, *self._error_observer.event_history]
+
+    @property
+    def error_events(self) -> list[VtkEvent]:  # numpydoc ignore=RT01
+        """List of VTK error events observed.
+
+        .. versionadded:: 0.47
+
+        """
+        return self._error_observer.event_history
+
+    @property
+    def warning_events(self) -> list[VtkEvent]:  # numpydoc ignore=RT01
+        """List of VTK error events observed.
+
+        .. versionadded:: 0.47
+
+        """
+        return self._warning_observer.event_history
+
+    @property
+    def _runtime_error_message(self) -> str:  # numpydoc ignore=RT01
+        """List of VTK error events formatted as runtime errors."""
+        return '\n'.join([str(e) for e in self.error_events])
+
+    @property
+    def _runtime_warning_message(self) -> str:  # numpydoc ignore=RT01
+        """List of VTK error events formatted as runtime errors."""
+        return '\n'.join([str(e) for e in self.warning_events])
+
+    def _raise_error(self, message: str):
+        raise VTKExecutionError(message)
+
+    def _emit_warning(self, message: str):
+        warn_external(message, VTKExecutionWarning)
 
 
 class VtkEvent(NamedTuple):
@@ -100,6 +191,16 @@
     path: str
     address: str
     alert: str
+    line: str
+    name: str
+
+    def __str__(self):
+        if all(self):
+            return (
+                f'{self.kind}: In {self.path}, line {self.line}\n'
+                f'{self.name} ({self.address}): {self.alert}'
+            ).strip()
+        return self.alert
 
 
 class Observer(_NoNewAttrMixin):
@@ -113,28 +214,40 @@
         store_history: bool = False,  # noqa: FBT001, FBT002
     ) -> None:
         """Initialize observer."""
-        self.__event_occurred = False
-        self.__message = None
-        self.__message_etc = None
+        self.__event_occurred: bool = False
+        self.__message: str | None = None
+        self.__message_etc: str | None = None
         self.CallDataType = 'string0'
-        self.__observing = False
+        self.__observing: bool = False
         self.event_type = event_type
         self.__log = log
 
         self.store_history = store_history
         self.event_history: list[VtkEvent] = []
+        self._event_history_etc: list[str] = []
 
     @staticmethod
-    def parse_message(message):  # numpydoc ignore=RT01
+    def parse_message(message) -> VtkEvent:  # numpydoc ignore=RT01
         """Parse the given message."""
-        # Message format
-        regex = re.compile(r'([A-Z]+):\sIn\s(.+),\sline\s.+\n\w+\s\((.+)\):\s(.+)')
-        try:
-            kind, path, address, alert = regex.findall(message)[0]
-        except Exception:  # noqa: BLE001
-            return '', '', '', message
-        else:
-            return kind, path, address, alert
+        regex = re.compile(
+            r'(?P<kind>[a-zA-Z]+):\sIn\s(?P<path>.+?),\sline\s(?P<line>\d+)\r?\n'
+            r'(?P<name>\w+) \((?P<address>0x[0-9a-fA-F]+)\):\s(?P<alert>.+)',
+            re.DOTALL,
+        )
+
+        match = regex.match(message)
+        if match:
+            d = match.groupdict()
+            kind = d.get('kind', '')
+            path = d.get('path', '')
+            line = d.get('line', '')
+            name = d.get('name', '')
+            address = d.get('address', '')
+            alert = d.get('alert', '').strip()
+            return VtkEvent(
+                kind=kind, path=path, line=line, name=name, address=address, alert=alert
+            )
+        return VtkEvent(kind='', path='', line='', name='', address='', alert=message.strip())
 
     def log_message(self, kind, alert) -> None:
         """Parse different event types and passes them to logging."""
@@ -152,12 +265,13 @@
         try:
             self.__event_occurred = True
             self.__message_etc = message
-            kind, path, address, alert = self.parse_message(message)
-            self.__message = alert
+            event = self.parse_message(message)
+            self.__message = event.alert
             if self.store_history:
-                self.event_history.append(VtkEvent(kind, path, address, alert))
+                self.event_history.append(event)
+                self._event_history_etc.append(message)
             if self.__log:
-                self.log_message(kind, alert)
+                self.log_message(event.kind, event.alert)
         except Exception:  # noqa: BLE001  # pragma: no cover
             try:
                 if len(message) > 120:
Index: python-pyvista/tests/core/test_utilities.py
===================================================================
--- python-pyvista.orig/tests/core/test_utilities.py	2026-01-30 19:08:44.499329438 +0100
+++ python-pyvista/tests/core/test_utilities.py	2026-01-30 19:11:55.874758108 +0100
@@ -638,19 +638,27 @@
 
 def test_progress_monitor():
     mesh = pv.Sphere()
-    ugrid = mesh.delaunay_3d(progress_bar=True)
-    assert isinstance(ugrid, pv.UnstructuredGrid)
+    ugrid = mesh.warp_by_vector(progress_bar=True)
+    assert isinstance(ugrid, pv.PolyData)
 
 
 def test_observer():
-    msg = 'KIND: In PATH, line 0\nfoo (ADDRESS): ALERT'
+    msg = 'KIND: In PATH, line 0\nfoo (0x000000): ALERT'
     obs = Observer()
     ret = obs.parse_message('foo')
     assert ret[3] == 'foo'
     ret = obs.parse_message(msg)
-    assert ret[3] == 'ALERT'
+    assert ret[0] == 'KIND' == ret.kind
+    assert ret[1] == 'PATH' == ret.path
+    assert ret[2] == '0x000000' == ret.address
+    assert ret[4] == '0' == ret.line
+    assert ret[5] == 'foo' == ret.name
+    assert ret[3] == 'ALERT' == ret.alert
+    assert str(ret) == msg
+
     for kind in ['WARNING', 'ERROR']:
         obs.log_message(kind, 'foo')
+
     # Pass positionally as that's what VTK will do
     obs(None, None, msg)
     assert obs.has_event_occurred()
@@ -664,6 +672,20 @@
         obs.observe(alg)
 
 
+def test_observer_default_event():
+    msg = 'This message does not match parsing regex!'
+    obs = Observer()
+    ret = obs.parse_message(msg)
+    assert ret.kind == ''
+    assert ret.path == ''
+    assert ret.address == ''
+    assert ret.line == ''
+    assert ret.name == ''
+    assert ret.alert == msg
+
+    assert str(ret) == msg
+
+
 @pytest.mark.parametrize('point', [1, object(), None])
 def test_valid_vector_raises(point):
     with pytest.raises(TypeError, match='foo must be a length three iterable of floats.'):
@@ -732,19 +754,21 @@
 
 def _generate_vtk_err():
     """Simple operation which generates a VTK error."""
-    x, y, z = np.meshgrid(
-        np.arange(-10, 10, 0.5), np.arange(-10, 10, 0.5), np.arange(-10, 10, 0.5)
-    )
-    mesh = pv.StructuredGrid(x, y, z)
-    x2, y2, z2 = np.meshgrid(np.arange(-1, 1, 0.5), np.arange(-1, 1, 0.5), np.arange(-1, 1, 0.5))
-    mesh2 = pv.StructuredGrid(x2, y2, z2)
+    from vtkmodules.vtkIOLegacy import vtkDataWriter
 
-    alg = vtk.vtkStreamTracer()
-    obs = pv.Observer()
-    obs.observe(alg)
-    alg.SetInputDataObject(mesh)
-    alg.SetSourceData(mesh2)
-    alg.Update()
+    # vtkWriter.cxx:55     ERR| vtkDataWriter (0x141efbd10): No input provided!
+    writer = vtkDataWriter()
+    writer.Write()
+
+
+def _generate_vtk_warn():
+    """Simple operation which generates a VTK warning."""
+    from vtkmodules.vtkFiltersCore import vtkMergeFilter
+
+    # vtkMergeFilter.cxx:277   WARN| vtkMergeFilter (0x600003c18000): Nothing to merge!
+    merge = vtkMergeFilter()
+    merge.AddInputData(_vtk.vtkPolyData())
+    merge.Update()
 
 
 def test_vtk_error_catcher():
@@ -753,7 +777,11 @@
     with error_catcher:
         _generate_vtk_err()
         _generate_vtk_err()
-    assert len(error_catcher.events) == 2
+        _generate_vtk_warn()
+        _generate_vtk_warn()
+    assert len(error_catcher.events) == 4
+    assert len(error_catcher.error_events) == 2
+    assert len(error_catcher.warning_events) == 2
 
     # raise_errors: False, no error
     error_catcher = pv.core.utilities.observers.VtkErrorCatcher()
@@ -762,13 +790,48 @@
 
     # raise_errors: True
     error_catcher = pv.core.utilities.observers.VtkErrorCatcher(raise_errors=True)
-    with pytest.raises(RuntimeError):
+    error_match = re.compile(
+        r'ERROR: In vtkWriter\.cxx, line \d+\n'
+        r'vtkDataWriter \(0x?[0-9a-fA-F]+\): No input provided!\n*'
+    )
+    with pytest.raises(RuntimeError, match=re.compile(error_match)):  # noqa: PT012
         with error_catcher:
             _generate_vtk_err()
-    assert len(error_catcher.events) == 1
+            _generate_vtk_warn()
+    assert len(error_catcher.events) == 2
+    assert len(error_catcher.error_events) == 1
+    assert len(error_catcher.warning_events) == 1
 
-    # raise_errors: True, no error
-    error_catcher = pv.core.utilities.observers.VtkErrorCatcher(raise_errors=True)
+    # Raise two VTK errors as a single RuntimeError
+    error_catcher = pv.core.utilities.observers.VtkErrorCatcher(
+        raise_errors=True, emit_warnings=True
+    )
+    error_match2 = re.compile(f'{error_match.pattern}\n{error_match.pattern}')
+    with pytest.raises(pv.VTKExecutionError, match=error_match2):  # noqa: PT012
+        with error_catcher:
+            _generate_vtk_err()
+            _generate_vtk_err()
+
+    # Warn and raise error. The order emitted by VTK is not guaranteed to be the same
+    # since warn and err events are logged independently.
+    # Here we generate VTK err then warn, but expect warning then error
+    error_catcher = pv.core.utilities.observers.VtkErrorCatcher(
+        raise_errors=True, emit_warnings=True
+    )
+    warning_match = re.compile(
+        r'Warning: In vtkMergeFilter\.cxx, line \d+\n'
+        r'vtkMergeFilter \(0x?[0-9a-fA-F]+\): Nothing to merge!'
+    )
+    with pytest.warns(pv.VTKExecutionWarning, match=warning_match):  # noqa: PT031
+        with pytest.raises(pv.VTKExecutionError, match=error_match):  # noqa: PT012
+            with error_catcher:
+                _generate_vtk_err()
+                _generate_vtk_warn()
+
+    # Test raise/emit with no errors/warnings generated
+    error_catcher = pv.core.utilities.observers.VtkErrorCatcher(
+        raise_errors=True, emit_warnings=True
+    )
     with error_catcher:
         pass
 
