← Back to all stages
Extra Stage: Error Handling Architecture
Concept
Design a robust error handling strategy that works across the entire codebase — from the lexer to the pager — with clear error propagation, context preservation, and user-friendly messages.
The Problem Today (Especially in C)
C has no exceptions, no Result<T, E>, no try/catch. Current approaches:
- Return
-1and hope the caller checks it (they often don't). - Use
fprintf(stderr, "[ERROR:00301] ...")and continue running (error is lost). - Call
exit(1)on any error (too aggressive — kills the REPL).
What It Teaches
- Error codes as a system: A well-designed error code scheme tells you WHAT failed, WHERE it failed, and WHY. The 5-digit scheme (
[ERROR:SSCCC]) encodes the stage and specific error. - Error propagation in C: Using return codes + an error context struct. Every function returns
DroidResult(success/failure) and sets error details in a thread-local or parameter-passedErrorContext. - Error recovery: The REPL should recover from any error and continue accepting input. Only catastrophic errors (out of memory, disk full) should terminate.
- Error context chain: Like PostgreSQL's
errcontext()— each layer adds context. The final error message shows the full chain: "Parser error at line 1, col 15: expected ')' but found ';'". - SQLSTATE codes: PostgreSQL uses a 5-character error classification (e.g.,
42P01= undefined table). The student implements a simplified version.
Error Architecture Design
// Error severity levels
typedef enum {
SEVERITY_INFO, // Informational (not an error)
SEVERITY_WARNING, // Warning (operation completed but something is off)
SEVERITY_ERROR, // Error (operation failed, can recover)
SEVERITY_FATAL, // Fatal (cannot continue, must exit)
} ErrorSeverity;
// Error context — carries full error information
typedef struct {
int code; // 5-digit error code (e.g., 301)
ErrorSeverity severity;
char message[256]; // Primary message
char detail[256]; // Detailed explanation
char hint[256]; // Suggestion for fixing
char source_file[64]; // __FILE__
int source_line; // __LINE__
char context[512]; // Accumulated context chain
} ErrorContext;
// Result type — every function returns this
typedef enum { DROID_OK, DROID_ERROR } DroidResult;
// Macros for ergonomic error reporting
#define DROID_RAISE(ctx, code, msg) \
do { (ctx)->code = (code); \
snprintf((ctx)->message, sizeof((ctx)->message), "%s", (msg)); \
(ctx)->source_line = __LINE__; \
return DROID_ERROR; } while(0)
#define DROID_TRY(expr) \
do { if ((expr) == DROID_ERROR) return DROID_ERROR; } while(0)
Error Code Scheme
[ERROR:SSCCC]
SS = Stage/Subsystem (00-33)
CCC = Specific error code within that subsystem
Subsystem codes:
00 = General / CLI
01 = Lexer
02 = Parser
03 = Executor
04 = Serialization
05 = Pager
06 = B-Tree
07 = Catalog / Schema
08 = Transaction
09 = WAL
10 = Planner
Examples:
[ERROR:02001] Parser: expected keyword, found 'xyz'
[ERROR:03001] Executor: table 'foo' does not exist
[ERROR:06003] B-Tree: page 5 is full, cannot insert
[ERROR:08001] Transaction: no active transaction for COMMIT
Learning Objectives
- Define a
DroidResulttype andErrorContextstruct. - Create
DROID_RAISEandDROID_TRYmacros for ergonomic error handling in C. - Refactor all functions to return
DroidResultand propagate errors upward. - Implement error context accumulation (each layer adds its context string).
- The REPL catches errors, formats them, and continues — never crashes.
- Errors include source file/line for debugging (not shown to end users).
Why This Matters
Poor error handling is the #1 source of bugs in C database engines. This stage forces the student to think systematically about failure modes, which is arguably more important than any SQL feature.