Inhaltsverzeichnis

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 Fehlerbehandlung.)

Fehlerbehandlung (Sprachebene) · Handle-Konzept · 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: std.fs_ext, std.pipe, std.signals, std.sched, std.mmap_ext, 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: std.db.sqlite, std.pdf.builder (intern), 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 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

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).