Fruehzeitig Scheitern
Wenn etwas scheitern wird, sollte es so frueh wie moeglich, so laut wie moeglich und mit so viel Kontext wie moeglich scheitern. Dieser Skill kodifiziert das Fail-Early-Muster: Eingaben an Systemgrenzen validieren, Guard-Klauseln verwenden, um schlechten Zustand abzulehnen bevor er sich ausbreitet, und Fehlermeldungen schreiben, die beantworten was scheiterte, wo, warum und wie man es behebt.
Wann verwenden
- Schreiben oder Pruefen von Funktionen, die externe Eingaben akzeptieren (Benutzerdaten, API-Antworten, Dateiinhalte)
- Eingabevalidierung zu Paketfunktionen hinzufuegen vor der CRAN-Einreichung
- Refaktorieren von Code, der still falsche Ergebnisse erzeugt statt Fehler zu werfen
- Pruefen von Pull Requests auf Fehlerbehandlungsqualitaet
- Absichern interner APIs gegen ungueltiger Argumente
Eingaben
- Erforderlich: Funktion oder Modul, auf das das Muster angewendet werden soll
- Erforderlich: Identifizierung von Vertrauensgrenzen (wo externe Daten eintreten)
- Optional: Bestehender Fehlerbehandlungscode zum Refaktorieren
- Optional: Zielsprache (Standard: R; gilt auch fuer Python, TypeScript, Rust)
Vorgehensweise
Schritt 1: Vertrauensgrenzen identifizieren
Kartieren, wo externe Daten ins System eintreten. Diese Punkte benoetigen Validierung:
- Oeffentliche API-Funktionen (exportierte Funktionen in einem R-Paket)
- Benutzerseitige Parameter
- Datei-E/A (Lesen von Konfigs, Datendateien, Benutzer-Uploads)
- Netzwerkantworten (API-Aufrufe, Datenbankabfragen)
- Umgebungsvariablen und Systemkonfiguration
Interne Hilfsfunktionen, die nur durch eigenen validierten Code aufgerufen werden, benoetigen generell keine redundante Validierung.
Erwartet: Eine Liste von Einstiegspunkten, an denen nicht vertrauenswuerdige Daten in den Code eintreten.
Bei Fehler: Falls Grenzen unklar sind, rueckwaerts von Fehlern in Logs oder Bug-Reports verfolgen, um herauszufinden, wo schlechte Daten zuerst eingetreten sind.
Schritt 2: Guard-Klauseln an Einstiegspunkten hinzufuegen
Eingaben am Anfang jeder oeffentlichen Funktion validieren, bevor irgendeine Arbeit beginnt.
R (base):
calculate_summary <- function(data, method = c("mean", "median", "trim"), trim_pct = 0.1) {
# Guard: type check
if (!is.data.frame(data)) {
stop("'data' must be a data frame, not ", class(data)[[1]], call. = FALSE)
}
# Guard: non-empty
if (nrow(data) == 0L) {
stop("'data' must have at least one row", call. = FALSE)
}
# Guard: argument matching
method <- match.arg(method)
# Guard: range check
if (!is.numeric(trim_pct) || trim_pct < 0 || trim_pct > 0.5) {
stop("'trim_pct' must be a number between 0 and 0.5, got: ", trim_pct, call. = FALSE)
}
# --- All guards passed, begin real work ---
# ...
}
R (rlang/cli — bevorzugt fuer Pakete):
calculate_summary <- function(data, method = c("mean", "median", "trim"), trim_pct = 0.1) {
rlang::check_required(data)
if (!is.data.frame(data)) {
cli::cli_abort("{.arg data} must be a data frame, not {.cls {class(data)}}.")
}
if (nrow(data) == 0L) {
cli::cli_abort("{.arg data} must have at least one row.")
}
method <- rlang::arg_match(method)
if (!is.numeric(trim_pct) || trim_pct < 0 || trim_pct > 0.5) {
cli::cli_abort("{.arg trim_pct} must be between 0 and 0.5, not {.val {trim_pct}}.")
}
# ...
}
Allgemein (TypeScript):
function calculateSummary(data: DataFrame, method: Method, trimPct: number): Summary {
if (data.rows.length === 0) {
throw new Error(`data must have at least one row`);
}
if (trimPct < 0 || trimPct > 0.5) {
throw new RangeError(`trimPct must be between 0 and 0.5, got: ${trimPct}`);
}
// ...
}
Erwartet: Jede oeffentliche Funktion beginnt mit Guard-Klauseln, die ungueltige Eingaben ablehnen, bevor Nebeneffekte oder Berechnungen beginnen.
Bei Fehler: Falls Validierungslogik lang wird (>15 Zeilen Guards), einen validate_*-Helfer extrahieren oder stopifnot() fuer einfache Typ-Assertions verwenden.
Schritt 3: Aussagekraeftige Fehlermeldungen schreiben
Jede Fehlermeldung sollte vier Fragen beantworten:
- Was scheiterte — welcher Parameter oder welche Operation
- Wo — Funktionsname oder Kontext (automatisch mit
cli::cli_abort) - Warum — was erwartet wurde vs. was erhalten wurde
- Wie zu beheben — wenn die Loesung nicht offensichtlich ist
Gute Meldungen:
# What + Why (expected vs. actual)
stop("'n' must be a positive integer, got: ", n, call. = FALSE)
# What + Why + How to fix
cli::cli_abort(c(
"{.arg config_path} does not exist: {.file {config_path}}",
"i" = "Create it with {.run create_config({.file {config_path}})}."
))
# What + context
cli::cli_abort(c(
"Column {.val {col_name}} not found in {.arg data}.",
"i" = "Available columns: {.val {names(data)}}"
))
Schlechte Meldungen:
stop("Error") # What failed? No idea
stop("Invalid input") # Which input? What's wrong with it?
stop(paste("Error in step", i)) # No actionable information
Erwartet: Fehlermeldungen sind selbstdokumentierend — ein Entwickler, der den Fehler zum ersten Mal sieht, kann ihn ohne Lesen des Quellcodes diagnostizieren und beheben.
Bei Fehler: Die drei juengsten Bug-Reports ueberpruefen. Falls einer das Lesen des Quellcodes erforderte, um ihn zu verstehen, muessen seine Fehlermeldungen verbessert werden.
Schritt 4: stop() gegenueber warning() bevorzugen
stop() (oder cli::cli_abort()) verwenden, wenn die Funktion kein korrektes Ergebnis erzeugen kann. warning() nur verwenden, wenn die Funktion noch ein sinnvolles Ergebnis liefern kann, aber der Aufrufer von einem Problem wissen sollte.
Faustregel: Falls ein Benutzer still eine falsche Antwort erhalten koennte, ist das ein stop(), kein warning().
# CORRECT: stop when result would be wrong
read_config <- function(path) {
if (!file.exists(path)) {
stop("Config file not found: ", path, call. = FALSE)
}
yaml::read_yaml(path)
}
# CORRECT: warn when result is still usable
summarize_data <- function(data) {
if (any(is.na(data$value))) {
warning(sum(is.na(data$value)), " NA values dropped from 'value' column", call. = FALSE)
data <- data[!is.na(data$value), ]
}
# proceed with valid data
}
Erwartet: stop() wird fuer Bedingungen verwendet, die falsche Ergebnisse erzeugen wuerden; warning() ist fuer degradierte-aber-gueltige Ergebnisse reserviert.
Bei Fehler: Bestehende warning()-Aufrufe pruefen. Falls die Funktion nach der Warnung Unsinn zurueckgibt, auf stop() aendern.
Schritt 5: Assertions fuer interne Invarianten verwenden
Fuer Bedingungen, die "niemals passieren sollten" in korrektem Code, Assertions verwenden. Diese fangen Programmiererfehler waehrend der Entwicklung auf:
# R: stopifnot for internal invariants
process_chunk <- function(chunk, total_size) {
stopifnot(
is.list(chunk),
length(chunk) > 0,
total_size > 0
)
# ...
}
# R: explicit assertion with context
merge_results <- function(left, right) {
if (ncol(left) != ncol(right)) {
stop("Internal error: column count mismatch (", ncol(left), " vs ", ncol(right),
"). This is a bug — please report it.", call. = FALSE)
}
# ...
}
Erwartet: Interne Invarianten werden assertiert, damit Bugs sofort an der Verletzungsstelle auftreten, nicht drei Funktionsaufrufe spaeter mit einem kryptischen Fehler.
Bei Fehler: Falls stopifnot()-Meldungen zu kryptisch sind, auf explizites if/stop mit Kontext umstellen.
Schritt 6: Anti-Muster refaktorieren
Diese gaengigen Anti-Muster identifizieren und beheben:
Anti-Muster 1: Leeres tryCatch (Fehler verschlucken)
# BEFORE: Error silently disappears
result <- tryCatch(
parse_data(input),
error = function(e) NULL
)
# AFTER: Log, re-throw, or return a typed error
result <- tryCatch(
parse_data(input),
error = function(e) {
cli::cli_abort("Failed to parse input: {e$message}", parent = e)
}
)
Anti-Muster 2: Standardwerte, die schlechte Eingaben verdecken
# BEFORE: Caller never knows their input was ignored
process <- function(x = 10) {
if (!is.numeric(x)) x <- 10 # silently replaces bad input
x * 2
}
# AFTER: Tell the caller about the problem
process <- function(x = 10) {
if (!is.numeric(x)) {
stop("'x' must be numeric, got ", class(x)[[1]], call. = FALSE)
}
x * 2
}
Anti-Muster 3: suppressWarnings als Loesung
# BEFORE: Hiding the symptom instead of fixing the cause
result <- suppressWarnings(as.numeric(user_input))
# AFTER: Validate explicitly, handle the expected case
if (!grepl("^-?\\d+\\.?\\d*$", user_input)) {
stop("Expected a number, got: '", user_input, "'", call. = FALSE)
}
result <- as.numeric(user_input)
Anti-Muster 4: Catch-All-Ausnahmebehandler
# BEFORE: Every error treated the same
tryCatch(
complex_operation(),
error = function(e) message("Something went wrong")
)
# AFTER: Handle specific conditions, let unexpected ones propagate
tryCatch(
complex_operation(),
custom_validation_error = function(e) {
cli::cli_warn("Validation issue: {e$message}")
fallback_value
}
# Unexpected errors propagate naturally
)
Erwartet: Anti-Muster werden durch explizite Validierung oder spezifische Fehlerbehandlung ersetzt.
Bei Fehler: Falls das Entfernen eines tryCatch kaskadierte Fehler verursacht, hat der Upstream-Code eine Validierungsluecke. Die Quelle beheben, nicht das Symptom.
Schritt 7: Das Fail-Early-Refaktoring validieren
Die Testsuite ausfuehren, um zu bestaetigen, dass Fehlerpfade korrekt funktionieren:
# Verify error messages are triggered
testthat::expect_error(calculate_summary("not_a_df"), "must be a data frame")
testthat::expect_error(calculate_summary(data.frame()), "at least one row")
testthat::expect_error(calculate_summary(mtcars, trim_pct = 2), "between 0 and 0.5")
# Verify valid inputs still work
testthat::expect_no_error(calculate_summary(mtcars, method = "mean"))
# Run full test suite
Rscript -e "devtools::test()"
Erwartet: Alle Tests bestehen. Fehlerpfad-Tests bestaetigen, dass schlechte Eingaben die erwartete Fehlermeldung ausloesen.
Bei Fehler: Falls bestehende Tests auf stillen Fehlern beruhten (z.B. NULL bei schlechter Eingabe zurueckgeben), sie aktualisieren, um den neuen Fehler zu erwarten.
Validierung
- Jede oeffentliche Funktion validiert ihre Eingaben, bevor sie arbeitet
- Fehlermeldungen beantworten: was scheiterte, wo, warum und wie zu beheben
-
stop()wird fuer Bedingungen verwendet, die falsche Ergebnisse erzeugen -
warning()wird nur fuer degradierte-aber-gueltige Ergebnisse verwendet - Keine leeren
tryCatch-Bloecke, die Fehler still verschlucken - Kein
suppressWarnings()als Ersatz fuer ordentliche Validierung - Keine Standardwerte, die ungueltige Eingaben still verdecken
- Interne Invarianten verwenden
stopifnot()oder explizite Assertions - Fehlerpfad-Tests fuer jede Validierungs-Guard existieren
- Testsuite besteht nach Refaktorierung
Haeufige Stolperfallen
Zu tief validieren: An Vertrauensgrenzen validieren (oeffentliche API), nicht in jedem internen Helfer. Uebermaessige Validierung fuegt Laerm hinzu und schadet der Performance.
Fehlermeldungen ohne Kontext:
"Invalid input"zwingt den Aufrufer zu raten. Immer den Parameternamen, den erwarteten Typ/Bereich und den tatsaechlich erhaltenen Wert einbeziehen.warning() verwenden wenn stop() gemeint ist: Falls die Funktion nach der Warnung Unsinn zurueckgibt, erhaelt der Aufrufer still eine falsche Antwort.
stop()verwenden und den Aufrufer entscheiden lassen, wie damit umzugehen ist.Fehler in tryCatch verschlucken:
tryCatch(..., error = function(e) NULL)versteckt Bugs. Falls gefangen werden muss, mit hinzugefuegtem Kontext protokollieren oder neu werfen.call. = FALSE vergessen: In R schliesst
stop("msg")standardmaessig den Aufruf ein, was fuer Endbenutzer laestig ist. In benutzerseitigen Funktionencall. = FALSEverwenden.cli::cli_abort()macht dies automatisch.In Tests statt in Code validieren: Tests pruefen Verhalten, schuetzen aber keine Produktions-Aufrufer. Validierung gehoert in die Funktion selbst.
Falsches R-Binary auf Hybrid-Systemen: Unter WSL oder Docker kann
Rscripteinen plattformuebergreifenden Wrapper statt nativem R aufloesen. Mitwhich Rscript && Rscript --versionpruefen. Das native R-Binary bevorzugen (z.B./usr/local/bin/Rscriptunter Linux/WSL) fuer Zuverlaessigkeit. Fuer die R-Pfadkonfiguration siehe Setting Up Your Environment.
Verwandte Skills
write-testthat-tests- Tests schreiben, die Fehlerpfade verifizierenreview-pull-request- Code auf fehlende Validierung und stille Fehler pruefenreview-software-architecture- Fehlerbehandlungsstrategie auf Systemebene beurteilencreate-skill- neue Skills nach dem agentskills.io-Standard erstellensecurity-audit-codebase- sicherheitsfokussierter Review, der sich mit Eingabevalidierung ueberschneidet