Handle Processing Feedback and Errors in PyQGIS

processing.run() looks like an ordinary function call, and it is not quite one. It can succeed while writing nothing, warn about something important without stopping, and report a failure through a channel your script never reads. In an interactive console none of that matters much. In a chain of eight algorithms running unattended at three in the morning, it is the difference between a fixable error and a directory of silently wrong outputs.

This recipe belongs to Chaining Processing Algorithms in PyQGIS. It covers the feedback object, subclassing it to route messages where you want them, making failures raise, adding progress and cancellation to a long job, and the difference between an algorithm error and a Python exception.

Four channels, and the two most scripts readAn algorithm reports through its return dictionary, through feedback messages at info, warning and error severities, and through exceptions. A script that passes no feedback object sees only the return value and any exception, so warnings and non fatal errors pass unnoticed.Two of these reach a script that passes no feedbackprocessing.run()one algorithmreturn dictOUTPUT: pathalways visiblepushWarning"3 features skipped"lost without feedbackreportErrornon-fatal by defaultlost without feedbackexceptionQgsProcessingExceptiona feedback object is the only way to see the middle two

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer with the Processing framework initialised.
  • A chain worth instrumenting; a single fast algorithm needs none of this.
  • For headless use, Processing initialised explicitly — see running scripts outside QGIS Desktop.

The default is quieter than you think

processing.run() raises QgsProcessingException when an algorithm fails outright. It does not raise for a warning, and it does not raise when an algorithm decides a bad input is survivable and continues with fewer features.

import processing

result = processing.run("native:buffer", {
    "INPUT": "/data/parcels.gpkg",
    "DISTANCE": 10,
    "OUTPUT": "/data/output/buffered.gpkg",
})
print(result)

Breakdown: With no feedback object the messages go to a default instance that discards them. If three features had invalid geometries and were skipped, that is a warning nobody sees, and the output has three fewer rows than the input. Comparing feature counts across a chain is a cheap defence, but reading the warnings is a better one.

A feedback object that routes messages

QgsProcessingFeedback has virtual methods for each severity, and subclassing it puts the messages wherever they belong.

from qgis.core import QgsProcessingFeedback, QgsMessageLog, Qgis


class LoggingFeedback(QgsProcessingFeedback):
    def __init__(self, tag="Processing", strict=False):
        super().__init__()
        self.tag = tag
        self.strict = strict
        self.warnings = []
        self.errors = []

    def pushInfo(self, info):
        QgsMessageLog.logMessage(info, self.tag, Qgis.Info)

    def pushWarning(self, warning):
        self.warnings.append(warning)
        QgsMessageLog.logMessage(warning, self.tag, Qgis.Warning)

    def reportError(self, error, fatalError=False):
        self.errors.append(error)
        QgsMessageLog.logMessage(error, self.tag, Qgis.Critical)
        if self.strict:
            raise RuntimeError(error)

    def setProgress(self, progress):
        super().setProgress(progress)

Breakdown: reportError receives a fatalError flag that the algorithm sets when it is about to give up; a non-fatal error means "something went wrong and I carried on", which is precisely the case worth converting into a raise in an unattended run. Collecting the messages into lists as well as logging them lets the calling code decide afterwards — if feedback.warnings: ... is often better than raising mid-run, because it lets the chain finish and report everything at once. Calling super().setProgress() matters if anything else is reading feedback.progress().

Use it, and act on what it collected

feedback = LoggingFeedback(tag="Nightly build")

result = processing.run(
    "native:buffer",
    {"INPUT": "/data/parcels.gpkg", "DISTANCE": 10,
     "OUTPUT": "/data/output/buffered.gpkg"},
    feedback=feedback,
)

if feedback.errors:
    raise RuntimeError(f"{len(feedback.errors)} error(s): {feedback.errors[0]}")
if feedback.warnings:
    print(f"completed with {len(feedback.warnings)} warning(s)")

Breakdown: Passing the same feedback object through every step of a chain accumulates the whole run's messages in one place, which is what you want in a log. Passing a fresh one per step and checking after each is what you want when a later step should not run after an earlier warning. Both are reasonable; mixing them by accident is not, so decide once per script.

One feedback for the chain, or one per stepSharing a single feedback object across a chain collects every message into one report at the end, which suits a log. Creating a fresh object per step allows the chain to stop as soon as a step reports a problem, which suits a pipeline where a later step would compound the error.Decide once whether a warning should stop the chainsharedbufferclipdissolveone reportper stepbuffer ✓clip ⚠ stopnot runnot run

Progress and cancellation

The same object carries progress reporting and a cancellation flag, which is what makes a long chain usable inside a plugin.

class ChainFeedback(LoggingFeedback):
    def __init__(self, steps, dialog=None, **kwargs):
        super().__init__(**kwargs)
        self.steps = steps
        self.step = 0
        self.dialog = dialog

    def next_step(self, name):
        self.step += 1
        self.pushInfo(f"[{self.step}/{self.steps}] {name}")

    def setProgress(self, progress):
        overall = ((self.step - 1) + progress / 100.0) / self.steps * 100.0
        if self.dialog:
            self.dialog.setValue(int(overall))
        super().setProgress(overall)

Breakdown: Each algorithm reports its own progress from 0 to 100, so a chain needs to fold those into an overall figure — which is what the arithmetic here does. Feeding it into a QProgressBar gives a bar that moves smoothly across the whole job rather than resetting eight times. isCanceled() on the feedback is checked by well-behaved algorithms, so calling feedback.cancel() from a button handler stops the current step and the loop can then break out, exactly as described in showing plugin progress and cancellation.

A runner that wraps the whole pattern

Rather than repeating the try/except and the message checks at every step, wrap them once.

import processing
from qgis.core import QgsProcessingException


def run_step(alg_id, params, feedback, label=None, strict_warnings=False):
    label = label or alg_id
    feedback.pushInfo(f"→ {label}")
    before_errors = len(feedback.errors)
    before_warnings = len(feedback.warnings)

    try:
        result = processing.run(alg_id, params, feedback=feedback)
    except QgsProcessingException as exc:
        feedback.reportError(f"{label} failed: {exc}", fatalError=True)
        raise

    new_errors = feedback.errors[before_errors:]
    new_warnings = feedback.warnings[before_warnings:]
    if new_errors:
        raise RuntimeError(f"{label} reported {len(new_errors)} error(s): {new_errors[0]}")
    if new_warnings and strict_warnings:
        raise RuntimeError(f"{label} reported {len(new_warnings)} warning(s): {new_warnings[0]}")
    if new_warnings:
        feedback.pushInfo(f"  {len(new_warnings)} warning(s) from {label}")
    return result

Breakdown: Slicing the message lists from a mark taken before the call is what attributes messages to the right step when a single feedback object is shared across a chain — without it, step five would re-report step two's warnings and fail forever. strict_warnings as a parameter rather than a constant lets one chain treat warnings as fatal while another logs them, which is usually the right split between a data-publishing pipeline and an exploratory one.

With this in place a chain reads as a list of steps and every one of them is instrumented identically, which is the property that makes a three-in-the-morning log actually readable. The same wrapper is a natural place to add timing, so the log records not only what happened but which step is the one worth optimising.

Failure modes worth distinguishing

Three quite different things all read as "it did not work", and the handling differs.

An invalid parameter — a missing file, a field that does not exist, a value out of range — raises QgsProcessingException from run() before the algorithm starts. This is a programming or configuration error and should propagate.

An algorithm failure during execution also raises, but the message is usually from the underlying library — GDAL, GEOS, SAGA — and is worth logging verbatim rather than wrapped, because the original text is what a search engine will match.

A partial success does not raise at all: features skipped, geometries repaired, values clamped. Only the feedback sees these, which is the entire argument for always passing one.

from qgis.core import QgsProcessingException

try:
    result = processing.run(alg_id, params, feedback=feedback)
except QgsProcessingException as exc:
    feedback.reportError(f"{alg_id} failed: {exc}", fatalError=True)
    raise

Breakdown: Re-raising after logging keeps the traceback intact while ensuring the failure also lands in whatever log the feedback writes to. Catching Exception broadly here is a mistake worth avoiding: a KeyboardInterrupt or a genuine bug in your own callback should not be reported as an algorithm failure.

Loading results, and when not to

processing.runAndLoadResults() runs the algorithm and adds its outputs to the project. It is convenient interactively and wrong in a batch.

import processing

processing.runAndLoadResults("native:buffer", {
    "INPUT": "/data/parcels.gpkg", "DISTANCE": 10,
    "OUTPUT": "/data/output/buffered.gpkg",
})

Breakdown: In a headless script there is no project the user will look at, so loading every intermediate wastes memory and can hold file handles open, which on Windows prevents a later step from overwriting the file. It also requires the full application context, so it fails in some minimal standalone setups. Use run() everywhere except in an interactive console, and add the final output to the project explicitly if it belongs there.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.16 LTR3.7QgsProcessingFeedback with pushInfo, reportError, setProgress.
3.22 LTR3.9pushWarning added as a distinct severity from pushInfo.
3.28 LTR3.9reportError gains the fatalError argument in Python bindings.
3.34 LTR3.12Baseline for this page.
3.40+3.12pushFormattedMessage available for structured log output.

Troubleshooting

  • The output has fewer features than the input and nothing said so. A warning was discarded. Pass a feedback object.
  • run() returned but the file is missing. The algorithm reported a non-fatal error. Check feedback.errors.
  • The progress bar jumps back to zero repeatedly. Each algorithm reports 0–100 separately. Fold them into an overall figure.
  • Cancelling does nothing. The running algorithm does not check isCanceled(). Nothing can be done except waiting for the step to finish.
  • runAndLoadResults fails headlessly. It needs the full application and a project. Use run().
  • The error message is unhelpfully wrapped. Log the original exception text verbatim as well as your own message.

Conclusion

Always pass a feedback object, subclass it to route messages to a real log, and decide explicitly whether a non-fatal error should stop the chain. Distinguish parameter errors from algorithm failures from partial successes, fold per-step progress into an overall figure, and keep runAndLoadResults() for the console.

Frequently Asked Questions

Can I see the algorithm's command line? For GDAL and SAGA algorithms, yes — the console command appears as an info message on the feedback, which is invaluable when reproducing a failure outside QGIS.

How do I silence an algorithm entirely? Pass a feedback subclass whose methods do nothing. That is different from passing None, which uses a default instance and still writes to the Processing log in a GUI session.

Does feedback work with qgis_process? The command-line runner has its own reporting and writes messages to stdout, with --json for machine-readable output. See using the qgis_process command line runner.

Can a custom algorithm push its own messages? Yes — inside processAlgorithm the feedback is passed in, and calling feedback.pushInfo() or feedback.reportError() makes your algorithm as observable as the built-in ones. See reporting progress and cancellation in a processing algorithm.