Error reporting in the libraries: what exists, and one rule to adopt

Status: a proposal for the maintainer. The first half records, with file and line, the ways the libraries report failure today. The second half proposes a single rule for new code. Nothing here changes behaviour, and no library has been changed to follow it.

Lines refer to core/compiler/lib_src/ as of this writing.

What is in use

1. A process-wide "last error" string: OpenAI, Gemini, Ollama, inference

The failing call returns Nil or false and leaves the reason in a class variable that the next call overwrites.

LibraryWhere it is keptNotes
OpenAI@last_message : static (openai.obs:6337), SetLastError (:6370), GetLastError (:6470)returns the text "<none>" when nothing failed. ParseBody (:6429) records the reason for every parse failure
Geminitwo copies: OAuthEndPoint (gemini.obs:15, GetLastError :97) and EndPoint (:2400, GetLastError :2523)returns Nil when nothing failed. A call through the other class does not see the first one's error
Ollama@last_error : static (ollama.obs:17), GetLastError (:26)returns Nil when nothing failed; Content (:101) turns "no response" into a recorded reason
InferenceEndPoint->SetLastError calls in inference.obs (21 of them; the first is :50)separate from API.Models.EndPoint, as its header says
Models, MCP servermodels.obs:351, net_server.obs:3121 (McpServer->GetLastError)the same shape again; ONNX has its own

Because the string is shared by every thread, two threads that fail at once can read each other's message. The three AI libraries also differ on the "no error" value ("<none>" versus Nil), and until recently several legacy methods printed ### Error: ... ### with ->ErrorLine() and returned Nil without recording anything (openai.obs:84, :172; gemini.obs has 26 such lines). A caller of those cannot ask why.

2. An instance error: ML, CSV, JSON, XML, NLP, Query columns

The object remembers why its last call failed; a failed call leaves the object usable.

This is safe with one thread per object. The "no error" value is "" in ML and Nil in most others.

3. A per-thread error: Query tables, sockets

GetError() answers for the calling thread's last call on the object, so several threads can share a Table or Catalog.

This is the convention that is correct under threads, and the one the runtime's own sockets use.

4. Raising: Runtime->Raise, typed try/on

5. Return value only

Many small methods (Vector->Set, Hash->Remove) say only whether they worked. That is fine when there is exactly one way to fail.

What goes wrong with the mix

Proposed rule

For new code, and for existing code the next time it is touched:

  1. A failure the caller should expect (I/O, network, parsing untrusted input, a missing record): return the failure value and keep the reason per thread. The failure value is Nil for an object or a string, false for an action, and -1 for a count or index. The reason is read with GetError(); it returns Nil when the calling thread's last call worked. Use the ThreadErrors shape in query.obs:1440, or the sockets' runtime-kept error, rather than a class variable. Never print and return.
  2. A failure that means the program is wrong (a precondition the caller broke: an empty training set, mismatched lengths, a fraction outside 0 to 1): Runtime->Raise an error that subclasses System.Error. Name it for the library (LabError), and document it on every function that can raise it. This is what ml_lab does today.
  3. New error classes subclass System.Error, so a caller can name one on(e : T) clause for a whole library, and finally still runs.
  4. Do not mix the two for one condition. A function that can fail for a reason in rule 1 does not also raise for it; a function that raises (rule 2) never returns Nil to say the same thing.
  5. A doc comment names the failure value and where the reason is read, the way Ollama's does (@return ... or Nil with the last error set).

What adopting it would touch

Nothing in this change. When the maintainer decides:

Open questions for the maintainer: whether the static last-error functions are public API that a deprecation period must cover, and whether ml should raise like ml_lab or ml_lab should return like ml.