Chapter Orientation

This chapter is one of the book's foundations. Nearly every fallible function in XRT reports failure the same way: return false or NULL, and place a structured xerror object into the current execution context's error slot. You already saw xrtGetError() called in Chapter 3; this chapter lays the model out completely — which fields make up an error, how the cause chain expresses "what actually went wrong below", who owns the error object, what the thread error slot's lifecycle is, and the engineering discipline of cross-thread transfer and wrapping timing. Every later chapter (networking, TLS, tasks) reuses these rules: you will see the combination "failure by return value, details in the error slot, layering by cause chain" over and over, so this chapter deserves a thorough read.

4.1引入

Start with a scenario every C programmer has met: you wrote a configuration-loading function.

str load_config(const char* sPath);

It returns NULL when the file doesn't exist. The caller gets NULL — and then? "File missing", "file exists but the JSON is invalid", and "out of memory" look identical in the return value. A common patch is printing a log line — but logs are for humans; upper layers cannot branch on them. Or define a pile of negative error codes — but each layer's code space collides with the others, and context like "line N, column M of the file is wrong" has nowhere to live.

Layering makes it worse. The config loader internally reads the file and parses JSON: when parsing fails, what the upper layer really needs to know is "config load failed, because line 3 of the JSON has a syntax error" — information from both layers must survive, not collapse into a bare parse failed.

Traditional solutions are each broken in their own way. Returning int error codes makes every module maintain a code table; crossing layers either loses context or collides code spaces. Printing straight to printf / stderr welds the information into the presentation layer — callers get neither category nor branchability. An errno-style global needs platform special-casing under threads and carries a single integer at a time. Fundamentally, these answers only say "it failed" — never "which layer, why, and what next".

These three problems — classification, context, layering — are what the XRT error model solves. It uses no error-code integer; instead an immutable object carries structured information and a human message together, and each execution context offers a "current error" slot, so a false return and the error details travel as a pair automatically.

4.2概念

What's inside an error object

xerror is immutable and safely holdable across threads. Once created, nobody can mutate its fields, which makes "passing it around" forever safe. Each error consists of:

  • Generic kind (xerrkind): a coarse classification stable across modules, e.g. XERR_TIMEOUT, XERR_NOT_FOUND. It is for control flow — decisions like "retry or not" and "degrade or not" look only at the kind.
  • Domain: a module-namespace string like "xrt.json", "example.net". The same numeric code means different things in different domains; the domain keeps codes from colliding.
  • In-module error code: each domain numbers its own from 1, for precise module-side decisions.
  • System code, operation name, UTF-8 message: bridging platform errno semantics, naming the failed operation, and a human-readable description respectively.
  • Optional data and the cause chain: machine-readable extras (say, the JSON error position) plus a link to the lower-layer error.

One discipline runs throughout: kind and domain are for machine decisions, messages are for display only. Never write code that parses error-message strings — branch on the kind when that's what you need, and read the structured fields for details.

The kind set is deliberately small and stable; the commonly used ones:

GroupKindMeaning
Input violates contractXERR_ARGUMENT XERR_TYPE XERR_VALUE XERR_RANGENull parameter, type mismatch, illegal value, out of range
State disallowsXERR_STATEObject or runtime state doesn't support this operation
ResourcesXERR_MEMORY XERR_IO XERR_NOT_FOUND XERR_EXISTS XERR_PERMISSIONOut of memory and system-resource errors
Retry laterXERR_AGAINObject still valid; retry after waiting
Wait terminatedXERR_TIMEOUT XERR_CANCELLED XERR_CLOSEDTimeout, cancellation, resource closed
Protocol & capabilityXERR_PROTOCOL XERR_UNSUPPORTED XERR_INTERNALProtocol error, capability missing, internal invariant broken

XERR_NONE only means "no error" and cannot be used to create an error object.

The cause chain: a layered story of failure

xrtErrorWrap "wraps" a lower-layer error into a new upper-layer error, forming a cause chain. Back to the opening config example, the chain looks like this:

1Lower layerJSON parse failed: the xrt.json domain records the line/column position
2Wrappingconfig load failed: the example.config domain wraps the parse error in XERR_IO
3DecisionxrtErrorIs sees XERR_IO at the top; the chain can be followed down to the syntax error
4DisplayxrtErrorMessage reads only the top message; the logging layer may traverse the whole chain

Deciding on the chain takes no manual traversal: xrtErrorIs(错误, XERR_TIMEOUT) searches the chain for the given kind and returns a borrowed pointer to the hit (or NULL). "Was this failure ultimately caused by a timeout" is one line of code. xrtErrorFind searches precisely by domain and code.

When to wrap: errors gain semantics at boundaries

A cause chain does not mean wrapping at every level. A practical discipline: wrap at module boundaries, pass through inside modules. An xrt.json-domain error raised inside the JSON module crosses the "config loading" boundary and becomes an example.config-domain "config load failed" — from then on the upper layer's retry and degradation strategy has a clear decision point: is it XERR_IO (retrying makes sense) or XERR_ARGUMENT (retrying is pointless — report a configuration error)? Low-level calls between boundaries need no extra "call failed" wrapper; otherwise the chain bloats with information-free duplicate nodes and the real semantics dilute.

The touchstone for "should I wrap": if the new layer can add decision information (a new domain, a new semantic kind, a new recovery strategy), wrap; if it merely restates "it failed below", don't — let the original error keep rising.

Division of labor between errors and logging

Error objects "carry failure structurally"; logging "leaves a trail of execution" — do not substitute one for the other. A common anti-pattern is printing the error message at every layer — the same event appears three times in the log, and troubleshooting loses the structure. A better division: errors pass quietly along the call chain and get handled at decision points (retry, degrade, final failure exit); logging records only at decision points and key boundaries, carrying the full kind and cause chain. Chapter 38's Logger can write xerror's structured fields into log records; for now, remember the division of labor.

The thread error slot: the current error

Every execution context owns one current-error slot. Plain threads default to the thread context; coroutines and task schedulers switch to their own slots, so coroutine migration never pollutes the hosting thread and tasks never cross-talk. The slot's semantics:

unsetSetError (the slot adds an object reference) →set
setClearError / SetError(NULL) →unset
setTakeError (takes ownership away) →unset

xrtGetError only borrows and changes nothing. Three key rules:

  • xrtSetError adds a reference to the passed object and replaces the current error; passing NULL is equivalent to clearing. On replace or clear, the slot releases the reference it held.
  • xrtTakeError takes the current reference and empties the slot — use it to keep an error long-term (log queues, cross-thread handoff) and remember xrtErrorFree when done.
  • Objects still sitting in the default slot when a native thread exits are freed automatically; extension threads need not force-clear errors to avoid leaks.

One default that's easy to trip on: successful operations do not implicitly clear an old error. The slot may still hold the previous failure — only after a function declares failure via its return value are you entitled to read the slot.

Cross-thread and long-term storage

xerror is immutable, so the object itself is safe to hold across threads; the only things needing care are "who frees it" and "which context owns the slot". To carry a failure from a worker thread back to the main thread (say, an error attached to a task result), the correct posture is xrtTakeError to take ownership, pass it along with the result, and xrtErrorFree at the consumer; storing the borrowed pointer from xrtGetError() is a race — the slot may be replaced at any moment. Coroutine and task error slots are managed by the scheduler and migrate with the execution context — you never carry them; only native threads need explicit handoff. Reference counting goes through xrtErrorRef/xrtErrorFree with the same rules as the general reference-counted objects of Chapter 5.

The one-step form for common failures

Most of the time you don't need the manual "create, set, free" trio. xrtSetErrorInfo creates and hands over in one step:

xrtSetErrorInfo(XERR_ARGUMENT, "app.config", 1, "path is empty");

For dynamic messages use xrtSetErrorFormat from the error_format module (printf rules, rejecting write-side-effect %n; on formatting failure it leaves an allocation-free XERR_MEMORY). Both helpers create immutable objects and hand them straight to the current context. To carry the source position, use xrtErrorBuildAt and read it back through xrtErrorFile, xrtErrorLine, xrtErrorColumn.

4.3示例

Complete program: create, wrap, and query a cause chain

The following program comes from the repository example examples/core/error/main.c, demonstrating a full error lifecycle under the ownership model. Read the code first, then see what it does:

examples/core/error/main.c源码 ↗






















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	




	 */
	xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "example.net", 1, "connect timeout");
	xerror* pError;

	if ( pCause == NULL ) {
		return 1;
	}

	




	 */
	pError = xrtErrorWrap(pCause, XERR_IO, "example.client", 2, "request failed");
	xrtErrorFree(pCause);
	if ( pError == NULL ) {
		return 2;
	}

	



	 */
	xrtSetError(pError);
	xrtErrorFree(pError);

	/* 借用读取:消息永不为 NULL,未设置错误时返回 "(no error)"。 */
	printf("error: %s\n", xrtErrorMessage(xrtGetError()));

	



	 */
	printf("timeout cause: %s\n",
		xrtErrorIs(xrtGetError(), XERR_TIMEOUT) != NULL ? "yes" : "no");

	/* 清空线程槽,避免错误泄漏到后续逻辑。 */
	xrtClearError();
	return 0;
}
$ gcc -O1 -I single impl.c examples/core/error/main.c -lws2_32 -liphlpapierror: request failedtimeout cause: yes

What just happened. The program takes four steps. First, xrtErrorCreate builds the lower-layer error (network timeout) from "kind + domain + code + message", returning an owning pointer — whoever creates it frees it. Second, xrtErrorWrap wraps it into an upper-layer XERR_IO; Wrap internally adds a reference to the cause, so the creator immediately xrtErrorFrees its own copy — whether the count reaches zero is the chain's business. Third, xrtSetError places it in the thread slot — note SetError also adds a reference, so the slot has its own copy; the creator releases its copy again, and at this moment the object has two referents (the chain and the slot). Fourth, xrtGetError borrows for reading: xrtErrorMessage never returns NULL (it returns (no error) when unset), and xrtErrorIs walks the chain and hits XERR_TIMEOUT. Finally xrtClearError empties the slot and releases the slot's reference.

Complete program: printf-style one-step construction

The second program comes from examples/core/error_format/main.c, showing convenient construction of dynamic messages:

examples/core/error_format/main.c源码 ↗



















 */

#include <stdio.h>

#include <xrt.h>



int main(void)
{
	







	 */
	xrtSetErrorFormat(
		XERR_NOT_FOUND,
		"example.config",
		1,
		"file does not exist: %s",
		"app.json"
	);

	/* 借用读取:消息已含动态上下文;未设置错误时返回 "(no error)"。 */
	printf("%s\n", xrtErrorMessage(xrtGetError()));

	/* 清空线程槽,避免错误泄漏到后续逻辑。 */
	xrtClearError();
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/core/error_format/main.c -lws2_32 -liphlpapifile does not exist: app.json

The difference from xrtErrorCreate is the message source: Create takes a complete string, Format takes a printf format string plus variadic arguments and internally reuses the string-formatting module before creating — error messages can carry runtime context (here, the file name) without the caller pre-assembling a buffer.

Note the second program never shows a single Free: xrtSetErrorFormat, like xrtSetErrorInfo, hands over to the current context right after creating — the creator no longer holds a reference, and the final xrtClearError releases the slot's copy. Compared with the first program's three steps of "create → set → free your copy", the one-step form saves more than lines — it eliminates the entire error class of "forgetting to free your copy" (Pitfall 2 of this chapter). In everyday code, prefer the helpers unless you are storing the error in a structure or carrying it across threads.

4.4契约

  • Ownership: Create/Wrap return owning pointers that must see exactly one Free; Wrap adds a reference to the cause; SetError adds a reference to the passed object; GetError only borrows.
  • Threading: xerror is immutable and holdable across threads; each execution context has its own error slot; to cross threads, TakeError first.
  • Error convention: false/NULL return values declare failure and the slot provides details; success does not clear the slot; read the slot only after seeing a failure return.
  • Display/judgment separation: control flow reads only the kind (domain+code when needed); messages and positions are for display and logs only.
  • Extension data: modules needing structured extras should define a stable format for Data instead of asking callers to parse display messages; xrtErrorBuildAt carries the source position, read back via xrtErrorFile/xrtErrorLine/xrtErrorColumn.
  • Minimal dependencies: the core error API depends on no containers, strings, or printf runtime; dynamic formatting is the separate error_format convenience module.

Common Mistakes

Pitfall 1: freeing a borrowed error:Pitfall 1: freeing a borrowed error

Symptoms: occasional crashes or heap corruption, often near code that "reads the error then cleans up".

Cause: xrtGetError returns a borrowed pointer; the slot may replace or clear it (releasing that reference) at any time; calling xrtErrorFree on a borrowed pointer is a double free.

错误写法
xerror* pError = xrtGetError();
printf("%s\n", xrtErrorMessage(pError));
xrtErrorFree(pError);   /* freed a borrow — double free when the slot clears */
正确写法
/* Use a borrow immediately; to hold it long-term, take ownership */
printf("%s\n", xrtErrorMessage(xrtGetError()));
/* or: xerror* pKeep = xrtTakeError(); ... xrtErrorFree(pKeep); */
Pitfall 2: forgetting to release your own cause reference after Wrap:Pitfall 2: forgetting to release your own cause reference after Wrap

Symptoms: slow memory growth; leak detectors report xerror objects that only increase.

Cause: after xrtErrorWrap adds a reference to the cause, the creator's original reference still exists and must be released by the creator — otherwise the cause object never reaches zero.

错误写法
xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "net", 1, "timeout");
xerror* pError = xrtErrorWrap(pCause, XERR_IO, "client", 2, "request failed");
/* missing xrtErrorFree(pCause): the cause object leaks */
正确写法
xerror* pCause = xrtErrorCreate(XERR_TIMEOUT, "net", 1, "timeout");
xerror* pError = xrtErrorWrap(pCause, XERR_IO, "client", 2, "request failed");
xrtErrorFree(pCause);
Pitfall 3: treating "the slot has an error" as the failure signal:Pitfall 3: treating "the slot has an error" as the failure signal

Symptoms: the function clearly succeeded, yet the upper layer reports a stale error; error-handling logic works intermittently.

Cause: successful operations don't implicitly clear old errors, and the slot may still hold the previous failure's object. Using "slot non-empty" as the failure criterion is deciding on stale state.

错误写法
call_something();              /* returned true — success */
if ( xrtGetError() != NULL ) { /* misjudged: this is left over from a previous failure */
	recover();
}
正确写法
if ( !call_something() ) {     /* see the failure return first */
	report(xrtErrorMessage(xrtGetError()));
}

Programming Exercises

Basic: create and read

Write a program: set an XERR_ARGUMENT error with xrtSetErrorInfo, print the message for its kind, then xrtClearError and confirm xrtErrorMessage(xrtGetError()) returns (no error).

Advanced: a three-layer cause chain

Build the three-layer chain "服务启动失败 → 配置加载失败 → JSON 第 N 行语法错误": the bottom layer via xrtErrorCreate (not Wrap), the middle and top via xrtErrorWrap. Query the bottom layer's kind with one xrtErrorIs call and print whether it hit. Hint: release your own cause reference immediately after each Wrap to avoid leaks.

Challenge: position-carrying failure logging

Implement a log_failure(const char* sFile, int iLine) macro: the failure path calls xrtErrorBuildAt (carrying the call-site source position automatically), then prints "file:line + message + every chain layer's message". Acceptance: build a two-layer cause chain and see both layers' messages plus the call-site file and line in the output; the position read back via xrtErrorFile/xrtErrorLine matches where the macro expanded. When done, return to the chapter's opening config-loading scenario and rewrite it to return structured errors — you will have a failure-handling skeleton ready for a real project.

Summary

TopicQuick reference
CreationxrtErrorCreate (owning) / xrtSetErrorInfo (one-step handover) / xrtSetErrorFormat (printf message)
LayeringxrtErrorWrap wraps a cause and adds a reference; xrtErrorIs searches the chain by kind; xrtErrorFind by domain and code
Slot semanticsSetError adds a reference and replaces; TakeError takes ownership; ClearError empties and releases; GetError only borrows
Decision timingRead the slot only after a failure return; success doesn't clear
One-step constructionxrtSetErrorInfo / xrtSetErrorFormat create and hand over — no leak window
Cross-threadObjects are immutable and cross-thread safe; carry via TakeError, Free at the consumer
Wrapping disciplineWrap at module boundaries, pass through inside; no new semantics, no wrap
Source positionWritten by xrtErrorBuildAt, read back by xrtErrorFile/xrtErrorLine/xrtErrorColumn
OwnershipThe creator must Free exactly once; borrows are never freed; TakeError before crossing threads
DisplayxrtErrorMessage never NULL; kinds for control flow, messages for humans

📖 API references in this chapter:error (34 entries)error_format (1 entries)core (25 entries)

Click for full signatures, parameter constraints, return values and error notes