====== Fehler-Konventionen der Standardbibliothek ====== Die Lyx-Standardbibliothek wirft keine Exceptions und kennt keine impliziten Fehlerzustände. Jede Funktion, die fehlschlagen kann, signalisiert das über ihren Rückgabewert — immer sichtbar, immer explizit. Diese Seite erklärt die vier Konventionen, die sich quer durch alle std-Units ziehen. (Die Sprache selbst kennt ''try''/''catch'', die Standardbibliothek nutzt es nicht — siehe [[lyx_-_programmiersprache:sprache:exception-handling|Fehlerbehandlung]].) → [[lyx_-_programmiersprache:sprache:exception-handling|Fehlerbehandlung (Sprachebene)]] · [[lyx_-_programmiersprache:sprache:handles-und-fd|Handle-Konzept]] · [[lyx_-_programmiersprache:sprache:rohspeicher|Rohspeicher]] ---- ===== Konvention 1: Kernel-Syscall → Rückgabe < 0 ===== Alle Wrapper um Linux-Syscalls (''open'', ''read'', ''write'', ''socket'', ''epoll_wait'' …) geben bei Fehler einen negativen Wert zurück. Dieser Wert ist der negierte errno-Code. import std.fs; import std.alloc; fn main(): int64 { var fd: int64 := open("/etc/hosts", 0, 0); if (fd < 0) { // fd enthaelt den negierten errno PrintLn("open fehlgeschlagen: errno = ", IntToStr(0 - fd)); return -1; } PrintLn("open /etc/hosts = ", IntToStr(fd), " (Deskriptor)"); var buf: int64 := alloc(4096); var n: int64 := read(fd, buf as pchar, 4096); if (n < 0) { close(fd); return -1; } PrintLn("read = ", IntToStr(n), " Bytes"); close(fd); // Die drei haeufigsten Fehlerfaelle im Vergleich PrintLn("open nicht-da = ", IntToStr(open("/gibt/es/nicht", 0, 0))); PrintLn("open /etc/shadow = ", IntToStr(open("/etc/shadow", 0, 0))); PrintLn("read auf fd 9999 = ", IntToStr(read(9999, buf as pchar, 16))); return 0; } Ausgabe (als gewöhnlicher Benutzer): open /etc/hosts = 3 (Deskriptor) read = 291 Bytes open nicht-da = -2 open /etc/shadow = -13 read auf fd 9999 = -9 Die Bytezahl beim Lesen richtet sich nach der Dateigröße — 291 ist die des ''/etc/hosts'' auf dem Prüfrechner. Die drei negativen Werte sind genau die negierten errno-Codes: ''-2'' = ENOENT (Datei fehlt), ''-13'' = EACCES (keine Berechtigung), ''-9'' = EBADF (ungültiger Deskriptor). Aus dem Rückgabewert wird der errno-Code also schlicht mit ''0 - fd'' zurückgewonnen. ^ Rückgabewert ^ Bedeutung ^ | ''≥ 0'' | Erfolg (oft: Anzahl gelesener Bytes, neuer fd, 0 = OK) | | ''< 0'' | Fehler; Wert = ''-errno'' (z.B. ''-2'' = ENOENT) | Alle Low-Level-Units nutzen diese Konvention: ''[[lyx_-_programmiersprache:units:fs_ext|std.fs_ext]]'', ''[[lyx_-_programmiersprache:units:pipe|std.pipe]]'', ''[[lyx_-_programmiersprache:units:signals|std.signals]]'', ''[[lyx_-_programmiersprache:units:sched|std.sched]]'', ''[[lyx_-_programmiersprache:units:mmap_ext|std.mmap_ext]]'', ''[[lyx_-_programmiersprache:units:net:epoll|std.net.epoll]]''. ---- ===== Konvention 2: Userspace-Handle → Rückgabe 0 bei Fehler ===== Funktionen, die einen Userspace-Handle (Pointer auf alloc'd Struct) erzeugen, geben **0** zurück wenn die Erstellung fehlschlägt. import std.db.sqlite; fn main(): int64 { var db: int64 := SQLiteOpen("/tmp/app.db"); if (db == 0) { PrintLn("Datenbank nicht geoeffnet"); return -1; } // Absichtlich fehlerhaftes SQL — Prepare liefert 0 var stmt: int64 := SQLiteStmtPrepare(db, "SELEKT * FROM users"); if (stmt == 0) { PrintLn("Prepare fehlgeschlagen: ", SQLiteErrmsg(db)); SQLiteClose(db); return -1; } return 0; } Ausgabe: Prepare fehlgeschlagen: near "SELEKT": syntax error Die eigentliche Fehlerursache steht also **nicht** im Rückgabewert — der ist bloß 0 — sondern wird mit ''SQLiteErrmsg(db)'' vom Handle abgeholt. Das ist der entscheidende Unterschied zu Konvention 1, wo der Rückgabewert selbst den errno-Code trägt. Ein Pfad in einem nicht vorhandenen Verzeichnis lässt bereits ''SQLiteOpen'' 0 liefern. Units die dieser Konvention folgen: ''[[lyx_-_programmiersprache:units:db:sqlite|std.db.sqlite]]'', ''[[lyx_-_programmiersprache:units:pdf:builder|std.pdf.builder]]'' (intern), ''[[lyx_-_programmiersprache:units:net:socket|std.net.socket]]''. ---- ===== Konvention 3: Zweistufige Prüfung (PostgreSQL-Muster) ===== Manche Verbindungen scheitern zweistufig: Die TCP-Verbindung kann erfolgreich sein, aber Auth oder Protokoll-Handshake danach fehlschlagen. Solche Units haben eine ''IsConnected''-Funktion. import std.db.postgres; var conn: int64 := PGConnect("127.0.0.1", 5432, "mydb", "user", "wrongpass"); // PGConnect gibt 0 zurück NUR bei TCP-Fehler (Host nicht erreichbar). // Bei falschem Passwort liefert es einen gültigen conn != 0. if (conn == 0) { PrintLn("TCP-Verbindung fehlgeschlagen"); return -1; } // Zweite Prüfung: Auth- oder Protokollfehler if (PGIsConnected(conn) == 0) { PrintLn("Verbindung aufgebaut, aber Auth-Fehler: ", PGError(conn)); PGClose(conn); return -1; } // Jetzt ist conn sicher verwendbar > Diese Konvention gilt für alle Protokolle mit einem mehrstufigen Handshake (TCP-Verbindung + Authentifizierung). Immer **beide** Prüfungen durchführen. ---- ===== Konvention 4: Fehlercode-Konstanten ===== Einige Units definieren eigene Rückgabe-Code-Konstanten anstelle von negativen Zahlen oder 0/1. Das trifft vor allem auf Units zu, die externe Protokolle wrappen. ==== SQLite ==== import std.db.sqlite; var rc: int64 := SQLiteStmtStep(stmt); if (rc == SQLITE_ROW) { // Zeile verfügbar — Column-Accessoren verwenden var name: pchar := SQLiteColumnText(stmt, 0); PrintLn(name); } else if (rc == SQLITE_DONE) { // Alle Zeilen verarbeitet — kein Fehler } else { // Fehler: rc enthält SQLite-Fehlercode PrintLn("Step-Fehler: ", IntToStr(rc)); // SQLITE_BUSY=5, SQLITE_ERROR=1, SQLITE_CONSTRAINT=19 … } ^ Konstante ^ Wert ^ Wann ^ | ''SQLITE_OK'' | 0 | Erfolg (nicht Step/Exec) | | ''SQLITE_ROW'' | 100 | Nächste Zeile verfügbar | | ''SQLITE_DONE'' | 101 | Alle Zeilen verarbeitet | | alle anderen | 1–99 | Fehler | ---- ===== Fehler weitergeben ===== Da die [[lyx_-_programmiersprache:units|Standardbibliothek]] ohne Exceptions arbeitet, wird ein Fehler durch Rückgabe eines Fehlerwerts nach oben propagiert. Das übliche Muster: import std.db.sqlite; // Gibt 0 bei Erfolg, -1 bei Fehler zurück fn LoadConfig(path: pchar): int64 { var db: int64 := SQLiteOpen(path); if (db == 0) { return -1; } var stmt: int64 := SQLiteStmtPrepare(db, "SELECT key, value FROM config"); if (stmt == 0) { SQLiteClose(db); return -1; } while (SQLiteStmtStep(stmt) == SQLITE_ROW) { PrintLn(" ", SQLiteColumnText(stmt, 0), " = ", SQLiteColumnText(stmt, 1)); } SQLiteStmtFinalize(stmt); SQLiteClose(db); return 0; // Erfolg } fn main(): int64 { // Testdatenbank anlegen var db: int64 := SQLiteOpen("/tmp/app.db"); SQLiteExec(db, "CREATE TABLE IF NOT EXISTS config(key TEXT, value TEXT)"); SQLiteExec(db, "INSERT INTO config VALUES('sprache','de'),('stufe','3')"); SQLiteClose(db); PrintLn("vorhandene Datenbank:"); var rc1: int64 := LoadConfig("/tmp/app.db"); PrintLn(" Rueckgabe = ", IntToStr(rc1)); PrintLn("fehlende Datenbank:"); var rc2: int64 := LoadConfig("/gibt/es/nicht/app.db"); PrintLn(" Rueckgabe = ", IntToStr(rc2)); return 0; } Ausgabe: vorhandene Datenbank: sprache = de stufe = 3 Rueckgabe = 0 fehlende Datenbank: Rueckgabe = -1 Zu beachten: Der Fehlerfall gibt ''-1'' zurück, **bevor** ''SQLiteStmtFinalize'' erreicht wird — deshalb steht in jedem vorzeitigen Ausstieg ein eigenes ''SQLiteClose''. Genau darum geht es im nächsten Abschnitt. ---- ===== Ressourcen bei Fehler freigeben ===== Der häufigste Fehler in Lyx-Code: Bei einem Fehler in der Mitte einer Initialisierungssequenz werden bereits erstellte Ressourcen nicht freigegeben. import std.fs; import std.db.sqlite; import std.db.postgres; // Schlecht — Memory/Resource Leak bei Fehler: fn BadInit(): int64 { var fd: int64 := open("/data/config", 0, 0); var db: int64 := SQLiteOpen("/data/app.db"); var conn: int64 := PGConnect("localhost", 5432, "db", "u", "p"); if (PGIsConnected(conn) == 0) { return -1; // fd und db wurden nie geschlossen! } return 0; } // Richtig — Cleanup bei jedem Fehlerfall: fn GoodInit(): int64 { var fd: int64 := open("/data/config", 0, 0); if (fd < 0) { return -1; } var db: int64 := SQLiteOpen("/data/app.db"); if (db == 0) { close(fd); return -1; } var conn: int64 := PGConnect("localhost", 5432, "db", "u", "p"); if (conn == 0 || PGIsConnected(conn) == 0) { SQLiteClose(db); close(fd); return -1; } // Alles OK — Ressourcen werden von der aufrufenden Schicht verwaltet return 0; } > **Faustregel:** Jede Ressource, die nach einem gescheiterten Schritt noch offen ist, muss freigegeben werden — entweder explizit im Fehler-Branch oder per ''defer''. Destruktoren hat Lyx nicht. ==== defer — Freigabe an der Blockgrenze ==== ''defer'' merkt einen Aufruf an der Quelltextstelle vor und führt ihn am **Ende des umgebenden Blocks** aus — nicht erst am Funktionsende. Mehrere Defers desselben Blocks laufen in **LIFO**-Reihenfolge, und zwar bevor die Blockvariablen verworfen werden. Der aufgeschobene Ausdruck darf also noch auf Variablen des Blocks zugreifen. import std.fs; import std.db.sqlite; fn LoadConfig(path: pchar): int64 { var fd: int64 := open(path, 0, 0); if (fd < 0) { return -1; } defer close(fd); // läuft am Ende dieses Blocks var db: int64 := SQLiteOpen("/data/app.db"); if (db == 0) { return -1; } // close(fd) läuft automatisch defer SQLiteClose(db); // ... Arbeit ... return 0; // LIFO: erst SQLiteClose(db), dann close(fd) } Beide Zusagen — LIFO und Blockgrenze — lassen sich in einem Durchlauf zeigen: fn Zeige(n: int64): void { PrintLn(" defer ", IntToStr(n)); } fn main(): int64 { defer Zeige(1); defer Zeige(2); defer Zeige(3); PrintLn("Rumpf zuerst:"); if (true) { defer Zeige(99); PrintLn(" im inneren Block"); } PrintLn(" nach dem inneren Block"); return 0; } Rumpf zuerst: im inneren Block defer 99 nach dem inneren Block defer 3 defer 2 defer 1 ''defer Zeige(99)'' läuft **vor** der Zeile „nach dem inneren Block" — es hängt am ''if''-Block, nicht an der Funktion. Die drei äußeren Defers folgen erst am Funktionsende, in umgekehrter Reihenfolge. ''break'' und ''continue'' lösen die Defers des Schleifenrumpfes ebenfalls aus — sie erreichen das Blockende zwar nie, überspringen die Freigabe aber trotzdem nicht. > **Fallstrick — Argumente werden erst am Blockende ausgewertet.** ''defer f(i)'' in einer Schleife sieht das dann aktuelle ''i'', nicht den Wert zum Zeitpunkt der Vormerkung. Wenn der Argumentwert festgehalten werden soll, ihn vorher in eine eigene Variable kopieren oder ein konstantes Argument verwenden. fn F(): int64 { var x: int64 := 1; defer recordStep(x); // gibt 42 aus, nicht 1 x := 42; return 0; } In einer **Schleife** tritt der Fallstrick nicht auf: Der Schleifenrumpf ist ein eigener Block, dessen Defers am Ende jeder Iteration laufen. Die Zählvariable hat dort noch ihren Wert für diesen Durchlauf: for i := 0; i < 3; i++ { defer recordStep(i); // 0, 1, 2 -- je am Ende des Durchlaufs } ---- ===== Schnell-Referenz ===== ^ Situation ^ Fehlerindikator ^ Beispiel ^ | Syscall / Low-Level I/O | ''< 0'' | ''open'', ''read'', ''EpollCreate'' | | Handle-Erstellung (Userspace) | ''== 0'' | ''SQLiteOpen'', ''PdfCreate'' | | Mehrstufiger Handshake | ''== 0'' + ''IsConnected == 0'' | ''PGConnect'' + ''PGIsConnected'' | | SQLite Step | ''SQLITE_ROW'' / ''SQLITE_DONE'' / Fehlercode | ''SQLiteStmtStep'' | | Binding / Column-Accessor | ''bool'' (true = Erfolg) | ''SQLiteBindInt'', ''SQLiteColumnIsNull'' | | Ressource freigeben | kein Rückgabewert | ''SQLiteClose'', ''PGClose'', ''PdfFree'' | → [[lyx_-_programmiersprache:sprache:exception-handling|Fehlerbehandlung auf Sprachebene (Result, Tuple, panic)]]\\ → [[lyx_-_programmiersprache:sprache:handles-und-fd|Handle-Konzept — Kernel-fd vs. Userspace-Handle]]\\ → [[lyx_-_programmiersprache:units:error|std.error — errno-Konstanten und Fehlermeldungen]] Letzte Aktualisierung: 2026-06-05 Codebeispiele geprüft: gegen **lyxc 1.2.5C** übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die ''pub fn''-Signaturen in ''aurum/std'' gehalten, 0 Abweichungen).