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 Fehlerbehandlung.)
→ Fehlerbehandlung (Sprachebene) · Handle-Konzept · Rohspeicher
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: std.fs_ext, std.pipe, std.signals, std.sched, std.mmap_ext, std.net.epoll.
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: std.db.sqlite, std.pdf.builder (intern), std.net.socket.
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.
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.
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 |
Da die 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.
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 perdefer. Destruktoren hat Lyx nicht.
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 aktuellei, 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
}
| 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 |
→ Fehlerbehandlung auf Sprachebene (Result, Tuple, panic)
→ Handle-Konzept — Kernel-fd vs. Userspace-Handle
→ 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).