You Caught the Exception. What Happens to the Half-Written Machine Code?

After catching an error, account for the state and output already produced.

Posted by Bruce Lee on 2026-06-14

Series contents · Resources and Scheduling · 阅读中文版

“The exception is caught. The compiler won’t crash.”

“Then why is only half the output directory there?”

“Because another path writes the files.”

Code-generation failures rarely stay inside one tidy try block. Shape validation may fail, a descriptor field may be impossible to encode, or a dependency count may be invalid. Sometimes only the final binary export reveals that an instruction does not fit the target format. Catching the exception is the first step. The next is deciding what the caller sees and what the system leaves behind.

Error handling starts as a protocol between layers

The changes reviewed here make operation-generation interfaces explicitly return success or failure, with optional diagnostic text. Inside the lower-level library, where exceptions are supported, a common wrapper catches both standard and unknown exceptions. The compiler checks the returned status and attaches the error to a specific operation location.

This closes a practical architectural gap. The lower-level library may use exceptions, while the compiler above it propagates failure explicitly and may even be built with exceptions disabled. Requiring every operation caller to write its own try/catch invites omissions and makes build options harder to coordinate.

The commit explicitly enables exceptions for the library and adds a boundary test program compiled with exceptions disabled. It tests whether failures can cross the interface through return values, without requiring the upper layer to handle a thrown exception.

A simplified interface looks like this:

1
2
3
4
result = generate_operation(config, diagnostic)
if result is failure:
report_at_current_operation(diagnostic)
stop_this_compilation

“Stop” matters here. A failure result does not automatically permit generating the next operation in the same generator.

Two different failure times need coverage

Some errors occur before generation: mismatched input and output shapes, invalid slice axes, or incorrect parameter-list lengths. Others occur during deferred encoding: an instruction object already exists, but its field ranges are not checked until binary export.

Checking only the operation-generation result is therefore insufficient. The verified changes also cover dependency generation, waits, releases, termination instructions, assembly dumps, incremental dumps, and binary export. Debug-information construction changes from returning an object directly to returning a fallible result, preventing an attempt to produce a debug file from becoming an escape route for an unhandled exception.

One test deliberately creates this situation: operation generation succeeds, binary encoding later fails, and the caller’s existing output vector must remain unchanged. That establishes the failure contract of the serialization output parameter. It does not establish that all internal generator state has been rolled back.

Why a status interface should clear old diagnostics

Suppose a caller reuses a string. An earlier failure leaves “parameter out of range” in it, and a later successful call does not clear it. The logging system may then see a success result alongside an old error. This does not change the machine code, but it can easily send a debugging effort in the wrong direction.

The common wrapper reviewed here clears the error text before the call, ensures that success leaves no stale message, and writes the specific exception information on failure. It still returns failure when no diagnostic pointer is supplied. Text is supplementary information; it must not be the only way to detect failure.

An interface annotation such as nodiscard can help find ignored status results, but its effectiveness still depends on warning policies and calling practices. If a caller drops the return value after the migration, the exception may stop propagating while the error is swallowed more quietly.

Do not call exception conversion a transaction

A transaction normally promises to commit all changes on success and restore a defined earlier state on failure. A code generator changes many things: instruction lists, pseudo-instruction lists, label numbers, incremental-read cursors, dependency-group counts, temporary resources, and memory-allocation summaries.

The wrapper verified here primarily catches exceptions and returns status. It does not create a unified snapshot of all those objects or undo every action after failure. A more accurate failure policy is therefore to stop and discard this generation context, unless an individual interface separately promises recovery.

The RAII discussed in Registers and RAII can return local resources, but it cannot automatically delete instructions already appended. Moving parameter validation ahead of pseudo-instruction insertion, as discussed in Encoding Boundaries, strengthens one particular interface’s guarantee that failure occurs before output changes. Local guarantees are valuable. Combining them does not establish a global transaction.

Why encode before exporting?

The relevant output logic moves complete binary encoding ahead of the creation and writing of subsequent artifacts. Debug assembly is likewise serialized in memory before file writing begins. This catches “the instructions cannot be encoded at all” earlier and reduces the chance of leaving incomplete output behind.

That still does not create a filesystem transaction. After encoding succeeds, insufficient disk space, permission errors, or process interruption can leave partial files. If the product requires an output directory to be either complete or absent, it needs mechanisms such as a temporary directory, integrity checks, and atomic publication.

Encoding early also has a cost. Holding the complete output in memory may increase peak usage. Evaluating that cost requires comparing artifact size, repeated encoding, and the diagnostic benefit on failure, rather than assuming every preparatory step is an unconditional improvement.

Helper functions must speak the same error language

The migration does more than replace top-level try/catch blocks. Helpers for axis normalization, array access, and shape validation during slice-descriptor construction now return explicit fallible results. One change deserves particular attention: when required memory information is unavailable, construction fails explicitly instead of returning a default descriptor that appears normal.

A default object can be a dangerous way to propagate an error. Zero-valued fields look like legitimate initial values. Later steps may continue allocating or encoding, only to report a misleading problem far from the cause. A fallible type distinguishes “no descriptor was constructed” from “a descriptor whose fields are all zero was constructed.”

The accompanying tests also adjust some shape-transformation enum cases. These changes should not be presented as new runtime capabilities. A better interpretation is that they align test calls with the currently supported encodings while retaining negative cases for invalid enum values.

The next mistake callers make after receiving failure

After migrating to status results, a common antipattern is to log the diagnostic and continue exporting. The log looks helpful, but the artifacts come from an incomplete context. Another is to reuse the same context to retry a different configuration without restoring dependency counts and output cursors that have already changed.

Interface documentation should therefore name the object’s state after failure: still usable, destruction only, or suitable for a specified limited retry. That adds just one condition beyond “returns success or failure,” yet determines whether callers can build reliable control flow.

Incremental dumps need particular care. If reading assembly succeeds and advances its cursor, but reading binary output then fails, a retry may produce empty assembly. One solution is to generate separate temporary results and commit the cursors only when everything succeeds. Another is to require discarding the context after any failure. Both policies can be sound. The stronger guarantees of the first must not be silently attributed to an implementation of the second.

There is another review question: which layer adds diagnostic context? The lower layer knows which field is invalid; the upper layer knows the operation and source location it belongs to. Combining that information is usually more useful than having the lower layer print a global error directly. It also lets tests check the failure’s location in the stack precisely. Avoid printing the same message again at every layer until one root cause looks like ten independent errors.

Regression matrix and fault-injection plan

Failure layer Example Required guarantee
Parameter validation Incorrect shape, axis, or list length Return failure and locate the diagnostic at the operation
Kernel generation An unrepresentable immediate Confine exceptions to the library boundary
Deferred encoding An unencodable data type or field Export fails explicitly without overwriting existing output
Dependency management An empty object or invalid count Do not treat failure as valid scheduling
Incremental dumping Earlier output kinds succeed and a later one fails Do not claim the whole sequence is retryable; define the discard policy
File writing Writing fails after encoding succeeds Distinguish generation failure from artifact-publication failure

The commit includes coverage for several boundaries and both positive and negative cases. The table suggests extending coverage with end-to-end fault injection.

An experiment could inject one controlled failure at each stage and record the returned status, diagnostic context, in-memory output, cursors, and files on disk. First define the basic guarantee that a failed context is discarded, then decide which interfaces merit stronger recovery. Avoid starting with expensive, complete rollback before establishing that any caller needs to retry.

Mature error handling means more than keeping the process alive. Every layer should be able to explain where this failure stopped, what it left behind, and what actions are allowed next.


Back to series contents · Previous · Next


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !