trap ERR vs. trap EXIT: Wann welcher Trap in Bash wirklich greift
AI generated
$_
#!/
Bash · Fehlerbehandlung · Traps · Debugging
trap ERR vs. trap EXIT
Wann welcher Trap in Bash wirklich greift, und wo beide Lücken haben

trap ERR und trap EXIT klingen wie zwei Varianten desselben Mechanismus, verhalten sich aber grundverschieden. EXIT feuert garantiert, ERR hat dieselben blinden Flecken wie set -e. Wer beide kombiniert, versteht wann eigene Stacktraces mit BASH_LINENO nötig werden, und wie man Fehler zuverlässig loggt statt sie stillschweigend zu verschlucken.

16 Min. Lesezeit trap ERR/EXIT · errtrace · BASH_LINENO Bash 4.x · 5.x · Linux

1. trap in Bash: Signale, Pseudo-Signale und der grundlegende Mechanismus

Das Kommando trap registriert einen Befehl oder eine Funktion, die Bash aufruft, sobald ein bestimmtes Ereignis eintritt. Neben echten Betriebssystem-Signalen wie SIGTERM oder SIGINT kennt Bash zusätzlich zwei Pseudo-Signale, die kein Kernel jemals verschickt, sondern die Bash selbst intern auslöst: EXIT und ERR. Beide lassen sich mit derselben Syntax registrieren, trap 'befehl' EXIT und trap 'befehl' ERR, verhalten sich beim Auslösen aber fundamental unterschiedlich, was in der Praxis regelmäßig zu falschen Annahmen führt.

Wer trap zum ersten Mal einsetzt, geht oft davon aus, dass ERR und EXIT einfach zwei Varianten desselben Cleanup-Mechanismus sind, einmal für den Fehlerfall und einmal für das normale Ende. Tatsächlich ist EXIT der zuverlässige Basismechanismus, der praktisch immer feuert, während ERR ein Werkzeug mit klar definierten, aber leicht zu übersehenden Ausnahmen ist. Diese Unterscheidung zu verstehen ist die Grundlage für jede robuste Fehlerbehandlung in produktiven Bash-Skripten.

2. EXIT: der Trap, der garantiert bei jedem Skriptende feuert

Ein trap ... EXIT wird ausgeführt, sobald das Skript beendet wird, unabhängig davon, ob es normal durchläuft, mit exit abbricht, ein unbehandeltes Signal empfängt oder wegen set -e vorzeitig terminiert. Genau diese Zuverlässigkeit macht EXIT zum Standardwerkzeug für Cleanup-Aufgaben: temporäre Dateien löschen, Locks freigeben, Hintergrundprozesse beenden. Ein einziger EXIT-Trap am Skriptanfang deckt praktisch jeden Ausstiegspfad ab, ohne dass an jeder Stelle im Code manuell aufgeräumt werden muss.

Wichtig ist, dass ein EXIT-Trap innerhalb einer Funktion standardmäßig nicht funktioniert wie erwartet: Traps sind grundsätzlich skriptweit gültig, nicht funktionslokal, und ein in einer Funktion registrierter EXIT-Trap überschreibt schlicht den zuvor gesetzten. Für Cleanup-Logik, die an eine bestimmte Funktion gebunden sein soll, braucht es entweder einen zentralen EXIT-Trap, der eine Liste von Cleanup-Funktionen abarbeitet, oder das Zurücksetzen des Traps am Ende der Funktion.


#!/usr/bin/env bash
set -euo pipefail

TMP_DIR="$(mktemp -d)"

cleanup() {
  local exit_code=$?
  rm -rf "$TMP_DIR"
  echo "Cleanup done, script exited with code $exit_code" >&2
}

# Fires on: normal exit, explicit exit, set -e abort, most signals
trap cleanup EXIT

echo "Working in $TMP_DIR"
false  # would abort here because of set -e; cleanup still runs

3. ERR: der Trap, der bei jedem fehlgeschlagenen Kommando feuert, außer wenn nicht

Ein trap ... ERR soll auslösen, sobald ein einfaches Kommando mit einem Rückgabewert ungleich null endet, ähnlich wie das Kriterium, das auch set -e für einen Skriptabbruch verwendet. In der Praxis heißt das: ERR ist das richtige Werkzeug, um beim ersten Fehler eine Diagnosezeile zu loggen, bevor das Skript beendet wird, statt am Ende nur einen nackten Exit-Code ohne Kontext zu haben.

Der entscheidende Haken ist, dass die Definition von einfaches Kommando in Bash enger ist, als die meisten erwarten. Ein fehlgeschlagener Befehl innerhalb einer if-Bedingung, nach && oder ||, oder als Teil einer Kommandoliste, deren Ergebnis explizit geprüft wird, löst den ERR-Trap in vielen Fällen nicht aus. Das ist keine Inkonsistenz, sondern exakt dasselbe Regelwerk, das auch bestimmt, wann set -e einen Abbruch auslöst und wann nicht.

4. Die Lücken von ERR: dieselben Kontexte, die auch set -e ignoriert

Weil ERR an dasselbe Kriterium wie set -e gekoppelt ist, gelten auch dieselben Ausnahmen. Ein Kommando in der Bedingung eines if, while oder until löst den Trap nicht aus, weil sein Rückgabewert bewusst als Steuerfluss abgefragt wird, nicht als Fehler. Ebenso feuert ERR nicht bei einem Kommando, das links von && oder rechts von || steht, weil auch dort der Exit-Code bewusst ausgewertet wird, statt einen unerwarteten Fehler zu signalisieren.

Eine weitere Lücke betrifft Pipelines: Ohne set -o pipefail zählt nur der Exit-Code des letzten Kommandos einer Pipe-Kette, sodass ein fehlschlagendes Kommando in der Mitte der Pipeline unbemerkt bleibt und den ERR-Trap nicht auslöst. Wer diese drei Lücken (Bedingungen, &&/||, Pipelines ohne pipefail) nicht kennt, verlässt sich fälschlich darauf, dass ERR jeden Fehler abfängt, und wundert sich später über Skripte, die trotz eines echten Fehlers klaglos durchlaufen.

5. set -o errtrace: ERR in Funktionen und Subshells vererben

Standardmäßig wird ein ERR-Trap, der im Hauptskript gesetzt ist, in Funktionen, Command-Substitutionen und Subshells nicht automatisch aktiv. Ein Fehler, der tief in einer aufgerufenen Funktion passiert, bleibt so unsichtbar für den äußeren Trap, obwohl genau dort die interessantesten Fehler auftreten. Die Option set -o errtrace (kurz set -E) schaltet dieses Verhalten um: Der ERR-Trap wird dann konsequent an Funktionsaufrufe und Subshells vererbt.

In der Praxis gehört set -o errtrace deshalb neben set -euo pipefail in praktisch jedes produktive Bash-Skript, das mit einem globalen ERR-Trap arbeitet, sonst greift die Fehlerbehandlung nur an der obersten Ebene und lässt genau die Fehler durchrutschen, die in tiefer verschachtelten Funktionen auftreten. Ohne diese Option wirkt ein ERR-Trap zuverlässiger, als er tatsächlich ist, was in größeren Skripten mit vielen Funktionen zu trügerischer Sicherheit führt.


#!/usr/bin/env bash
set -euo pipefail
set -o errtrace  # equivalent to set -E: propagate ERR into functions

report_error() {
  echo "ERROR in ${FUNCNAME[1]:-main} at line ${BASH_LINENO[0]}" >&2
}
trap report_error ERR

deploy_step() {
  # Without errtrace, a failure here would NOT trigger the outer trap
  rsync -a --delete ./build/ /var/www/app/
}

deploy_step

6. Eigene Stacktraces bauen mit BASH_LINENO und FUNCNAME

Bash pflegt zwei parallele Arrays, die zusammen einen vollständigen Aufrufstapel ergeben: FUNCNAME enthält die Namen aller aktuell aktiven Funktionen, von der innersten bis zur äußersten, und BASH_LINENO enthält für jeden dieser Aufrufe die Zeilennummer, an der die jeweils nächsttiefere Funktion aufgerufen wurde. Aus beiden Arrays lässt sich innerhalb eines ERR-Traps ein vollständiger, menschenlesbarer Stacktrace zusammensetzen, der genau zeigt, über welche Funktionsaufrufe ein Fehler entstanden ist.

Für produktive Deployment- oder Wartungsskripte lohnt sich eine kleine, wiederverwendbare Funktion, die diesen Stacktrace bei jedem ERR-Ereignis ausgibt und wahlweise an ein Log-System weiterreicht. Dabei ist wichtig, den Trap-Body als Funktionsaufruf zu registrieren statt als komplexen Inline-String, damit die Arrays im richtigen Kontext ausgewertet werden und keine Quoting-Fehler entstehen, die gerade bei verschachtelten Anführungszeichen in Trap-Strings ein häufiges Problem sind.


#!/usr/bin/env bash
set -euo pipefail
set -o errtrace

print_stacktrace() {
  local i
  echo "Stacktrace (most recent call first):" >&2
  for ((i = 0; i < ${#FUNCNAME[@]} - 1; i++)); do
    echo "  at ${FUNCNAME[$i]}() called from line ${BASH_LINENO[$i]}" >&2
  done
}
trap print_stacktrace ERR

level_two() { grep "pattern" /nonexistent-file; }
level_one() { level_two; }

level_one

7. ERR und EXIT kombinieren: einmal loggen, garantiert aufräumen

In robusten Skripten übernehmen ERR und EXIT unterschiedliche, sich ergänzende Aufgaben: ERR ist für die Diagnose zuständig, es protokolliert an genau der Stelle, an der ein Fehler auftritt, mit Zeilennummer und Funktionsname, während EXIT unabhängig vom Erfolg oder Misserfolg garantiert aufräumt. Beide Traps gleichzeitig zu setzen ist kein Widerspruch, sondern das übliche Muster: ERR liefert den Kontext, EXIT liefert die Garantie.

Ein häufiger Fehler ist, im ERR-Trap direkt exit aufzurufen und dabei zu vergessen, dass dieser Aufruf selbst wieder den EXIT-Trap auslöst. Das ist meistens erwünscht, kann aber zu doppelter Ausgabe führen, wenn der EXIT-Trap denselben Fehlerstatus erneut loggt. Ein sauberes Muster speichert den Exit-Code einmal in einer Variable und reicht ihn zwischen den Traps weiter, statt ihn mehrfach neu zu berechnen oder zu erraten.

8. Praxisbeispiel: ein Deployment-Skript mit vollständiger Fehlerbehandlung

In einem echten Deployment-Skript zeigt sich der Wert dieser Kombination am deutlichsten: Schlägt ein Schritt wie rsync, eine Datenbankmigration oder ein Health-Check fehl, soll das Team sofort eine aussagekräftige Meldung erhalten, inklusive der Funktion und Zeile, in der es passiert ist, und gleichzeitig soll das Skript garantiert Sperrdateien freigeben und temporäre Verzeichnisse entfernen, egal wie es endet.

Das folgende Muster kombiniert einen ERR-Trap mit Stacktrace-Ausgabe, einen EXIT-Trap für Cleanup und set -o errtrace, damit auch Fehler in verschachtelten Funktionen zuverlässig erfasst werden. Dieses Grundgerüst lässt sich für die meisten produktiven Bash-Skripte praktisch unverändert übernehmen und ist deutlich robuster als ein einzelner globaler trap ... EXIT ohne gezielte Fehlerdiagnose.


#!/usr/bin/env bash
set -euo pipefail
set -o errtrace

LOCK_FILE="/var/lock/deploy.lock"

on_error() {
  local exit_code=$?
  echo "DEPLOY FAILED (exit $exit_code) in ${FUNCNAME[1]:-main} at line ${BASH_LINENO[0]}" >&2
}
trap on_error ERR

on_exit() {
  rm -f "$LOCK_FILE"
}
trap on_exit EXIT

acquire_lock() { : > "$LOCK_FILE"; }
run_migrations() { bin/magento setup:upgrade; }
health_check() { curl -fsS https://shop.example.com/health >/dev/null; }

acquire_lock
run_migrations
health_check
echo "Deploy successful

9. ERR und EXIT im Vergleich: wann welcher Trap die richtige Wahl ist

Wer beide Traps kennt, wählt sie gezielt nach Aufgabe statt aus Gewohnheit: EXIT für alles, was unabhängig vom Ausgang garantiert passieren muss, ERR für die gezielte Diagnose an der Fehlerstelle selbst. Ein Skript, das nur EXIT nutzt, räumt zwar zuverlässig auf, verliert aber den genauen Ort des Fehlers. Ein Skript, das nur ERR nutzt, protokolliert zwar präzise, riskiert aber verwaiste Ressourcen, wenn ein Fehlerpfad den Trap aus einem der bekannten Gründe nicht auslöst.

Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen und hilft bei der Entscheidung, welcher Trap für welchen Zweck in einem Skript zum Einsatz kommen sollte, inklusive der bekannten Lücken, die ERR mit set -e teilt.

Merkmal trap ... EXIT trap ... ERR Praxis-Empfehlung
Auslöser Jedes Skriptende, normal oder abnormal Fehlgeschlagenes einfaches Kommando Beide gemeinsam registrieren
Bedingungen (if, while) Nicht relevant, feuert immer Löst nicht aus Fehler dort manuell prüfen
&&, || Nicht relevant Löst nicht aus Bewusst als Steuerfluss behandeln
Funktionen/Subshells Immer aktiv Nur mit set -o errtrace errtrace immer setzen
Typischer Einsatz Cleanup, Locks freigeben Diagnose, Stacktrace loggen ERR für Kontext, EXIT für Garantie

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

trap ERR vs. trap EXIT: Das Wichtigste auf einen Blick

EXIT

Feuert garantiert bei jedem Skriptende, normal oder abnormal. Das richtige Werkzeug für Cleanup-Aufgaben wie Locks und temporäre Dateien.

ERR

Feuert bei fehlgeschlagenen einfachen Kommandos, aber nicht in if-Bedingungen, nach && oder ||, oder in Pipelines ohne pipefail.

errtrace

set -o errtrace (set -E) vererbt ERR in Funktionen und Subshells. Ohne diese Option bleiben Fehler in tieferen Funktionen unsichtbar.

Stacktrace

FUNCNAME und BASH_LINENO liefern zusammen Funktionsname und Zeilennummer für jede Ebene des Aufrufstapels innerhalb eines ERR-Traps.

11. FAQ: trap ERR vs. trap EXIT: Das Wichtigste auf einen Blick

1Warum löst mein trap ... ERR bei einem offensichtlichen Fehler nicht aus?
Vermutlich steht der Befehl in einer if-Bedingung, nach && oder ||, oder in einer Pipeline ohne pipefail. Das sind exakt dieselben Kontexte, in denen auch set -e nicht abbricht.
2Reicht set -e allein aus, oder brauche ich zusätzlich trap ... ERR?
set -e beendet das Skript, liefert aber keinen Kontext zum Fehler. trap ... ERR ergänzt das um Diagnose-Informationen wie Zeilennummer und Funktionsname vor dem Abbruch.
3Was macht set -o errtrace genau?
Es sorgt dafür, dass ein ERR-Trap auch innerhalb von Funktionen, Command-Substitutionen und Subshells ausgelöst wird, statt nur im Hauptskript-Kontext.
4Feuert trap ... EXIT auch bei einem harten Signal wie SIGKILL?
Nein. SIGKILL kann von keinem Prozess abgefangen werden, auch nicht von Bash selbst, deshalb läuft in diesem Fall kein Trap mehr.
5Wie lese ich BASH_LINENO richtig?
BASH_LINENO[i] enthält die Zeile, an der die in FUNCNAME[i] genannte Funktion aufgerufen wurde. Index 0 ist die zuletzt aufgerufene, innerste Funktion.
6Kann ich mehrere ERR-Traps gleichzeitig registrieren?
Nein, jeder neue trap ... ERR-Aufruf überschreibt den vorherigen. Für mehrere Aktionen sollte eine einzelne Funktion registriert werden, die intern mehrere Schritte ausführt.
7Löst ERR auch bei einem fehlgeschlagenen Kommando in einer Command-Substitution aus?
Nur mit set -o errtrace. Ohne diese Option wird ein Fehler innerhalb von $(...) genauso wenig gemeldet wie ein Fehler in einer Funktion.
8Soll ich im ERR-Trap direkt exit aufrufen?
Meist nicht nötig, da set -e das Skript nach dem ERR-Trap ohnehin beendet. Ein expliziter exit-Aufruf kann außerdem den EXIT-Trap ein zweites Mal mit demselben Fehler konfrontieren.
9Warum feuert mein ERR-Trap in einer Pipeline nicht?
Ohne set -o pipefail zählt nur der Exit-Code des letzten Kommandos der Pipe-Kette. Ein Fehler in der Mitte bleibt unbemerkt, solange das letzte Kommando erfolgreich endet.
10Ist trap ... DEBUG dasselbe wie trap ... ERR?
Nein. DEBUG feuert vor jedem einzelnen Kommando im Skript und dient meist dem Tracing, nicht der Fehlerbehandlung. ERR feuert ausschließlich bei fehlgeschlagenen Kommandos.