Sane Coding Blog Notes on Sane C++ Libraries and practical systems work.

🍂 Sane C++ September 26

Welcome to the September 2026 update! This month replaces text-based errors with structured Result identities, adds Unix-domain sockets and external HTTP transports, improves the agent skills, and fixes several asynchronous lifetime and cancellation issues.

Structured Result errors

The biggest change this month is the error model shared by the libraries.

Previously, SC::Result stored a pointer to an error message. It was small and easy to propagate, but distinguishing failures meant inspecting English text. That also tied error identity to wording and made variable diagnostics depend on the lifetime of a formatted buffer.

The new Result stores a 32-bit category and a 32-bit error value in one eight-byte object. Zero represents success. Each library owns its error enum and category, while a small registry assigns the built-in category numbers and checks for collisions. Applications can define their own categories too.

This makes errors useful to code as well as to people. A caller can distinguish capacity exhaustion, invalid input, cancellation, or an unsupported operation through an explicit identity. Equivalent failures use the same primary code on each platform; native error numbers and backend stages belong in additional context.

Some APIs return an enriched result carrying that context: an operation kind, required capacity, offset, or native error number. Converting it to a plain Result preserves the category and primary error, while dropping the extra detail. SC_TRY continues to propagate failures, and SC_CO_TRY does the same for coroutines.

Error text is now optional. Each owner has a dedicated error header and a separate opt-in formatter header. Formatters write UTF-8 into caller-provided storage, report the required capacity, and leave applications free to supply their own wording or translations. Core libraries do not need to carry canonical English messages when an application only wants numeric errors. Release binary checks cover this separation for static, shared, and single-file builds on macOS, Linux, and Windows.

The temporary compatibility bridge used during migration is gone. The old message-pointer factories and SC_TRY_MSG have been removed, and all participating binaries need to be rebuilt together. This is a source and ABI change, even though plain Result ends the migration at eight bytes again.

The shared definitions are Common source fragments, used by Foundation and the other libraries without adding a new inter-library dependency. The architecture and formatting contracts are recorded in COMMON-0009.

Detailed list of commits:

The migration across libraries and tools

Most of September's commits are the work needed to make that model consistent across the repository.

The system-facing libraries now report portable operation errors and retain native details where useful: Threading, Process, SerialPort, File, FileSystem, FileSystemWatcher, and Plugin.

FileSystemIterator also separates normal exhaustion from failure. Reaching the end of a directory traversal no longer needs an error sentinel, and a native enumeration failure stays distinguishable from an empty directory. Filesystem predicates similarly separate their yes/no answer from an operation error. Plugin scanning uses an explicit completion state as well.

The migration continues through Cryptography, Fibers, and the async and HTTP layers. Tests now assert error identities and propagation instead of matching prose. In particular, foreign-category failures must survive wrappers without being replaced by a generic local error, and inactive enriched context must be cleared.

Build and Package have their own tool categories. A failed toolchain selection, package receipt validation, archive extraction, or download can be classified at that boundary without adding tool-specific errors to a library enum. Strings supplies structured command-line failures and the CLI still presents readable diagnostics when its optional formatters are enabled.

There are too many individual producer migrations to list usefully here, so these are the main entry points and final cleanup commits.

Selected commits:

Unix-domain sockets and HTTP transports

Socket now supports Unix-domain streams and datagrams through family-neutral addresses. Pathname endpoints work on macOS and Linux, and Linux also supports abstract names. Windows and Emscripten report this socket family as unsupported. Filesystem socket paths remain caller-owned: closing a descriptor does not remove the pathname.

The same addressing support reaches Async and Await, so local IPC can use synchronous calls, callbacks, or coroutines without switching to a separate socket API.

Http gained external asynchronous transports. A client connector can own connection establishment before the built-in DNS/socket path starts, then supply readable and writable plaintext streams. A server can accept externally supplied stream pairs into its caller-owned connection slots. Both paths use the existing HTTP/1.1 parser and state machine.

This allows native transport providers to own their handles and connection setup while HTTP stays independent of TLS and platform-specific transport APIs. Connection retirement and reuse follow the installed streams' lifetime barriers.

HttpClient also received session-state fixes. Cookie and authorization updates check capacity before committing copied fields, so a short scratch buffer cannot leave a partially updated slot. Plain HTTP responses cannot create or replace Secure cookies, IP-literal cookie hosts require exact domains, and malformed cookie input is handled more carefully. The libcurl backend disables signals when used from a worker thread.

Detailed list of commits:

Skills and evaluation

The repository now separates the Sane C++ Libraries skill, which helps agents compose the actual APIs, from the Sane C++ Style skill, which teaches ownership, capacity, errors, cleanup, and lifetime design without requiring Sane libraries.

The API guidance is more concrete now. It directs agents to verify public headers and examples before writing code, and includes a process-and-pipe composition recipe. Capturing stdout and stderr is a useful test case: a process exit does not mean both pipes have drained, a full capture buffer does not mean reading should stop, and a fixed number of slots is only useful if each slot can be safely reused.

I also added a small evaluation workflow with recorded attempts and acceptance suites. The latest recorded attempt passed all six declared functional suites, but a supplemental probe still found an unreaped child. Those are limited experiments, with changes in both skills and model between some runs, so I would not turn the scores into a general claim about agent performance.

They did give useful feedback: the skill gained timed-wakeup and queue-progress guidance, and the library gained a fix to reap POSIX children when process-exit completion is delivered. The evaluation notes keep the observed failure and the subsequent API fix visible.

Detailed list of commits:

Async lifetime fixes and other work

Several changes this month deal with what happens just after an asynchronous callback runs.

Async now copies a due timeout callback before invoking it and restarts traversal from the live list. The callback may wake a thread that immediately releases its request, so retaining the request's callable or a cached next pointer was unsafe. A separate fix avoids touching an idle sequence after its completion callback, since that callback may resume a coroutine and destroy or reuse the awaiter that owned the sequence.

AsyncFibers now synchronizes cancellation handoff with the event-loop owner thread. Stop and completion callbacks finish before the cancellation result is written, avoiding concurrent writes that became visible during the Result migration. Async also dispatches cancellations before it can block waiting for more events.

AsyncStreams handles failed reads without a buffer, emits finish after a deferred writable end, and preserves transform-finalization and zlib errors. HTTP WebSocket upgrade failures are contained, and the handshake SHA-1 provider can be selected explicitly.

The remaining work includes bounded Visual Studio discovery in Plugin, backend-free cryptography assertions for Fil-C, updated documentation, and the usual monthly LOC refresh.

Detailed list of commits:

See you next month!