A caller invokes a function that reports no fault, receives its error, and enters the error branch anyway. The two comparisons that expose the contradiction can sit beside each other:
var pointer *Fault
err := flawed(false)
fmt.Println(pointer == nil) // true
fmt.Println(err == nil) // falseThe pointer is nil. The error interface is not. Nothing is wrong with the caller’s ordinary if err != nil check. The defect is earlier: returning pointer from a function declared to return error converts the nil Fault into an interface that has a dynamic type, Fault, and a nil dynamic value. That interface is a real, non-nil value for comparison purposes, so the caller takes its error path. This Go nil pointer error interface mismatch is established behavior, not a new compiler feature.
This playbook follows that value through its producer, a forwarding function, a %w wrapper, errors.Is, and errors.As. It then repairs only the success representation: success becomes a literal nil error interface, while a genuine *Fault{Code: "E_SYNTH"} remains detectable and keeps its diagnostic meaning.
The evidence is deliberately local and bounded. The fixture uses only the standard library, ordinary pointers and interfaces, and a nil-safe Error method. The commissioned run identifies Go 1.23.2 on linux/amd64, nine named cases, and a source hash. The goal is not to redefine nil handling across Go. It is to qualify one error-return contract without hiding real failures.
Define the no-error result before debugging the caller
Start with the contract, not with the surprising log line. In this fixture, the producer has only two legitimate outcomes:
Producer intent | Required return value | Required consumer branch |
Success | A nil error interface | SUCCESS |
Synthetic failure | A non-nil *Fault whose Code is E_SYNTH | ERROR |
That definition makes err != nil the correct consumer test. Under this function's contract, a caller should treat a non-nil error as an error. Changing the caller to recognize one producer's malformed success value would spread the defect and make each consumer responsible for reconstructing intent.
The observable contract is the branch selected from the returned value. A string such as "nil fault", an empty log field, or a formatter’s output is only diagnostic material. It cannot turn a non-nil interface into success. This distinction matters even in broader API error-handling boundaries, but the present test does not require an HTTP endpoint, status code, authentication layer, or remote request.
The acceptance rule is therefore narrow: return literal nil on the no-error path, return the real Fault on the failure path, and verify both. A test that exercises only success could be made green by deleting all error returns. A test that exercises only failure could miss the typed-nil regression entirely. Both sides are part of one contract.
Separate a concrete pointer from an interface value
A declaration such as var f Fault creates a concrete pointer variable whose zero value is nil. The comparison f == nil is therefore true. Because Fault has an Error() string method, it implements the built-in error interface. That method set permits a conversion from the concrete *Fault value to error; it does not erase the pointer’s concrete type.
The official Go FAQ on nil error values illustrates this exact boundary with a nil concrete error pointer returned as error. It explains the result schematically as an interface with a dynamic type and a nil dynamic value, then recommends returning an explicit nil for the no-error case. The same page warns that the caller otherwise sees a non-nil error even though the concrete pointer is nil.
For the normative comparison rule, use the Go language specification rather than inferring a machine representation. The specification says interface values are equal when their dynamic types are identical and their dynamic values are equal, or when both interface values are nil. A nil interface has no dynamic type; an interface containing a nil Fault has dynamic type Fault. Those are different comparison states.
The maintained FAQ and specification were consulted on October 14, 2026; no individual publication date is asserted for either maintained page. The FAQ’s type/value pair is a useful language model, not permission to claim a particular number of machine words, header fields, addresses, or implementation layout. None of those details is needed to diagnose this defect.
Name the conversion that changes the comparison
The critical statement is not the caller’s if err != nil. It is the return in a function whose result type is error:
func flawed(fail bool) error {
var f *Fault
if fail {
f = &Fault{Code: "E_SYNTH"}
}
return f
}At return f, Go supplies the declared error result. On the success path, the dynamic type becomes *Fault while the dynamic value is the nil pointer. The resulting interface is non-nil. Blaming the later comparison would reverse ownership: the comparison accurately reports the interface it received. The producer must express absence as return nil.
Run the complete nine-case Go driver
Save the following source as go_nil_fixture.go in a writable disposable directory. The listing is the known-answer fixture used by the commissioning evidence. With LF line endings and the final newline shown, its SHA-256 is 5b36f61c5004608fbd29495361ca8dfa34904ad1a52c6e7a27b0edfe10a4cabc. Changing whitespace, comments, line endings, or source changes that identity even when behavior appears equivalent.
The hash answers “which bytes were run,” not “were those bytes correct.” Behavioral correctness comes from the literal case manifest and assertions. Conversely, matching behavior from edited source does not preserve the commissioned artifact’s identity. Retain both dimensions: source identity for traceability and case-level outcomes for qualification. On a workstation that writes CRLF line endings, either preserve the commissioned LF file or record the new hash honestly rather than claiming the old one.
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
)
type Fault struct { Code string }
func (f Fault) Error() string {
if f == nil { return "nil fault" } // Deliberately safe; not true of all methods.
return f.Code
}
func flawed(fail bool) error {
var f Fault
if fail { f = &Fault{Code:"E_SYNTH"} }
return f // A typed nil pointer is converted to an error interface.
}
func repaired(fail bool) error {
if fail { return &Fault{Code:"E_SYNTH"} }
return nil
}
func forward(e error) error { return e }
func wrap(e error) error {
if e != nil { return fmt.Errorf("operation: %w", e) }
return nil
}
type Evidence struct {
ID string json:"id"
Status string json:"status"
IsNil bool json:"is_nil"
DynamicType string json:"dynamic_type"
IsNilByErrorsIs bool json:"errors_is_nil"
AsFault bool json:"as_fault"
TargetNil bool json:"target_nil"
Branch string json:"branch"
Text string json:"text"
}
func require(ok bool, msg string) { if !ok { panic(msg) } }
func observe(id string, e error) Evidence {
var target Fault
r := Evidence{ID:id, Status:"FAIL", IsNil:e==nil,
DynamicType:fmt.Sprintf("%T",e), IsNilByErrorsIs:errors.Is(e,nil),
AsFault:errors.As(e,&target), TargetNil:target==nil, Branch:"SUCCESS"}
if e != nil { r.Branch="ERROR"; r.Text=e.Error() }
return r
}
func main() {
, source, , ok := runtime.Caller(0)
if !ok { panic("Cannot locate source") }
data, err := os.ReadFile(source); if err != nil { panic(err) }
sum := sha256.Sum256(data)
dir, err := os.MkdirTemp(filepath.Dir(source),"go-nil-run-")
if err != nil { panic(err) }
rows := []Evidence{}
report := map[string]any{"fixture":"go-typed-nil-v1","status":"FAIL",
"started_utc":time.Now().UTC().Format(time.RFC3339Nano),
"go":runtime.Version(),"os":runtime.GOOS,"arch":runtime.GOARCH,
"source_sha256":hex.EncodeToString(sum[:])}
failed := false
func() {
defer func() { if problem:=recover(); problem!=nil {
failed=true; report["error"]=fmt.Sprint(problem)
} }()
var pointer Fault
report["concrete_pointer_is_nil"]=pointer==nil
require(pointer==nil,"Concrete control failed")
var empty error
// Literal oracles: nil interface, As match, As target nil, error text.
specs:=[]struct{id string; value error; nilValue, match, nilTarget bool; text string}{
{"G0",empty,true,false,true,""},
{"G1",pointer,false,true,true,"nil fault"},
{"G2",flawed(false),false,true,true,"nil fault"},
{"G3",flawed(true),false,true,false,"E_SYNTH"},
{"G4",repaired(false),true,false,true,""},
{"G5",repaired(true),false,true,false,"E_SYNTH"},
{"G6",forward(flawed(false)),false,true,true,"nil fault"},
{"G7",wrap(flawed(false)),false,true,true,"operation: nil fault"},
{"G8",wrap(repaired(false)),true,false,true,""},
}
for , s:=range specs {
rows=append(rows,observe(s.id,s.value))
r:=&rows[len(rows)-1]
require(r.IsNil==s.nilValue,s.id+": wrong interface nil comparison")
require(r.IsNilByErrorsIs==s.nilValue,s.id+": wrong errors.Is result")
require(r.AsFault==s.match && r.TargetNil==s.nilTarget,s.id+": wrong As result")
require(r.Text==s.text,s.id+": wrong safe error text")
if s.nilValue { require(r.Branch=="SUCCESS",s.id+": wrong success branch")
} else { require(r.Branch=="ERROR",s.id+": wrong error branch") }
r.Status="PASS"
}
require(len(rows)==9,"Incomplete ledger")
report["status"]="PASS9_CASES"
}()
report["cases"]=rows
report["finished_utc"]=time.Now().UTC().Format(time.RFC3339Nano)
encoded, err:=json.MarshalIndent(report,""," "); if err!=nil { panic(err) }
path:=filepath.Join(dir,"results.json")
if err=os.WriteFile(path,append(encoded,'\n'),0600);err!=nil { panic(err) }
fmt.Println(path)
if failed { os.Exit(1) }
}Run it as a single file. No go.mod, network access, service, or third-party dependency is needed:
mkdir -p /tmp/go-typed-nil-check
cd /tmp/go-typed-nil-check
go version
sha256sum go_nil_fixture.go
result_path="$(go run go_nil_fixture.go)"
printf '%s\n' "$result_path"
cat "$result_path"The program prints the path of a temporary results.json; capturing that path runs the fixture once and keeps the JSON tied to one set of timestamps. Inspect the file rather than treating the path or a zero-looking console message as evidence. Preserve the source and its report together. The fixture records runtime.Version(), operating system, architecture, source hash, start and finish timestamps, the concrete-pointer control, the overall status, and each completed case.
The supplied commissioning record reports:
Fixture: go-typed-nil-v1
Environment: go1.23.2, linux, amd64
Started: 2026-10-14T06:41:15.777283876Z
Finished: 2026-10-14T06:41:15.77730659Z
Source SHA-256: 5b36f61c5004608fbd29495361ca8dfa34904ad1a52c6e7a27b0edfe10a4cabc
Overall status: PASS_9_CASES
Those timestamps identify one execution; their short interval is not a benchmark.
Treat fields in the report according to their provenance. The environment, timestamps, hash, dynamic types, strings, and branches are observations from a particular execution. The expected values in specs are authored requirements. The final ACCEPT_BOUNDED, REPAIR, or HOLD label is a review decision derived from both. Keeping those categories separate prevents a local observation from being promoted into a universal guarantee and prevents a reviewer from quietly changing an oracle to fit unexpected output.
The specs slice is the literal oracle. It does not learn expected values from output. For every case it states four independent expectations: whether the interface is nil, whether errors.As matches *Fault, whether the resulting target pointer is nil, and the safe error text. The loop then derives and checks the selected branch. This is useful repeatable regression evidence: the test names the behavior it will accept before reading the observed values.
The nine cases are intentionally not nine variations of the same call. G0 and G1 establish empty-interface and boxed-pointer controls. G2 and G3 exercise flawed success and real failure. G4 and G5 qualify the repaired pair. G6 tests forwarding, G7 tests wrapping a defective value, and G8 tests wrapping a repaired nil. Together they locate the first bad conversion and show that the repair survives downstream boundaries without deleting a genuine fault.
Several design details make failures inspectable:
Status begins as FAIL and changes to PASS only after all assertions for that row succeed.
Rows are appended before their assertions. An assertion panic inside the protected function leaves the rows completed so far in the report and records the panic text.
report["status"] remains FAIL unless all nine cases and the ledger-length check finish.
The process writes JSON and exits non-zero when a protected assertion failed.
Failures before the protected block, such as inability to locate or read the source, or failures while encoding or writing the report can prevent a report from being produced. Absence of a report is therefore not success.
The test also records DynamicType with %T. That field is evidence about the observed run, not an API promise about an unexported wrapper implementation. The behavioral oracle does not require a specific package-private wrapper type name.
The maintained errors package documentation describes As as finding the first matching error in the error tree, assigning it to the target, and returning true; it describes Is as matching by equality or an error type's Is method while traversing wrappers. The maintained page also contains APIs added after Go 1.23.2; this fixture uses only errors.Is and errors.As, both documented as added in Go 1.13. The official Go 1.13 error-handling article was published on October 17, 2019 and explains the Go 1.13 additions, including %w wrapping. These facilities are established, not features introduced in the batch year.
Keep diagnostic agreement separate from application acceptance
Every row can pass its expected assertion while some rows still demonstrate an unacceptable application contract. G1, G2, G6, and G7 are negative controls: they intentionally carry a nil *Fault inside a non-nil error path. Their PASS status means the fixture reproduced the counterexample exactly. It does not mean the producer should be released unchanged.
This distinction prevents an aggregate PASS_9_CASES from becoming a false approval label. The aggregate means the implementation behaved exactly like the nine-case manifest. Release judgment is a second step: accept the repaired success path and real failures, repair any boundary that manufactures a non-nil success error, and hold when the ledger or identity evidence is incomplete.
Compare an empty error with a boxed nil pointer
G0 and G1 isolate the difference before any producer function runs. The concrete control begins with var pointer *Fault, and pointer == nil is true. The two cases then place either no value or that pointer into an error-typed position.
Check | G0: var empty error | G1: value: pointer |
Concrete pointer control | nil | nil |
e == nil | true | false |
Observed dynamic type | <nil> | *main.Fault |
Selected branch | SUCCESS | ERROR |
Safe text | empty | nil fault |
G0 is a nil interface. %T reports <nil>, errors.As finds no Fault, and the caller selects success. G1 contains the nil pointer as its dynamic value, but the interface has dynamic type main.Fault. The interface comparison is false, errors.As matches that type, and the caller selects error.
The origin story, "both came from an absent pointer," does not determine interface equality. Nor does a human-friendly rendering. A logger could omit an empty field, print <nil>, or invoke Error and print nil fault; those are presentation decisions. The contract fields are the interface comparison, dynamic type, As state, and chosen branch.
G1 is especially valuable because it removes the producer's conditional logic. It shows that the surprising branch does not depend on flawed, wrapping, JSON, or a service framework. The conversion of one nil *Fault into error is enough.
This is the practical meaning behind a Go typed nil error. A search such as “Go error is not nil” often starts from the caller, but the decisive evidence is the pair of values crossing the return boundary. The useful question is not whether the pointer was nil locally; it is whether the returned error interface itself is nil after conversion.
Reproduce the flawed producer on success and failure
G2 calls flawed(false). Inside the function, f remains a nil Fault, but the declared result type is error. Returning f creates the same interface state as G1. The result is not nil, its dynamic type is main.Fault, errors.As matches, its target is nil, the safe text is nil fault, and the selected branch is ERROR. This is the application defect: a path intended as success is represented as an error.
G3 calls flawed(true). This time f points to Fault{Code: "E_SYNTH"} before conversion. The returned interface is non-nil. errors.As matches and sets a non-nil target; the fixture constructed that Fault with Code set to E_SYNTH. The observed text is E_SYNTH, and the branch is ERROR.
Keeping G3 beside G2 prevents a counterfeit repair. Replacing return f with unconditional return nil would make G2 appear fixed but would delete the required failure signal. A bounded repair must change only the no-error path. The real failure’s type, code, and branch remain independent acceptance checks.
Make the nil-safe Error method an explicit fixture choice
The fixture defines:
func (f *Fault) Error() string {
if f == nil {
return "nil fault"
}
return f.Code
}Calling a method through a nil pointer receiver can execute when the method itself handles the nil receiver without dereferencing it. This method deliberately does so, allowing G1, G2, G6, and G7 to produce stable text instead of letting a panic obscure the interface comparison.
That safety is local to Fault.Error. It does not establish that arbitrary error types tolerate nil receivers, that every formatter will produce the same text, or that dereferencing a nil receiver is safe. The string nil fault is an authored diagnostic oracle, not a language-defined rendering of every typed nil error.
Return literal nil without discarding a genuine fault
The repair is direct and intentionally unsurprising:
func repaired(fail bool) error {
if fail {
return &Fault{Code: "E_SYNTH"}
}
return nil
}G4 calls repaired(false). It receives a nil error interface, so e == nil and errors.Is(e, nil) are true, %T reports <nil>, errors.As does not match, and the selected branch is SUCCESS. G5 calls repaired(true) and still receives a non-nil *Fault with code and text E_SYNTH; errors.As matches a non-nil target and the selected branch remains ERROR.
That pair is the bounded repair. It neither weakens the caller's check nor converts genuine errors into success. This repair is distinct from a separate Go toolchain migration workflow: the observed environment is Go 1.23.2, and the qualification concerns an established interface contract, not adoption of a new language feature.
Avoid preserving the defective shape merely to keep one shared return statement:
func stillFlawed(fail bool) error {
var f *Fault
if fail {
f = &Fault{Code: "E_SYNTH"}
}
return f
}The shorter-looking common return is exactly where success acquires a dynamic type. Explicit branches communicate the contract and make the success value literal. G4 and G5 show that clarity did not cost the failure signal.
A named error result does not improve this by itself. It is safe only when the success path leaves that result as the nil interface or assigns literal nil. Assigning a nil *Fault to the named error result reproduces the same conversion. Review the value assigned to the interface, not whether the function uses named results, early returns, or a shared epilogue.
Check why errors.Is does not normalize the value
errors.Is is a matching operation, not a nil normalizer. The errors package documentation says it examines an error tree and reports a match when the error equals the target or when an applicable Is(error) bool method returns true. Fault defines no custom Is method in this fixture.
Consequently, errors.Is(e, nil) tracks the relevant interface distinction here:
G0, G4, and G8 contain nil error interfaces, so both e == nil and errors.Is(e, nil) are true.
G1, G2, G3, G5, G6, and G7 are non-nil interfaces, so both checks are false.
For the typed-nil cases, the inner pointer is nil, but e is not equal to the nil target. Traversing G7’s wrapper reaches another non-nil error interface containing the typed nil; it does not discover a literal nil error that should be reclassified as success.
Keep the direct e == nil field beside errors.Is(e, nil) in the ledger. Their agreement in this fixture is useful evidence, but it does not change their roles: direct equality selects the ordinary success/error branch, while errors.Is is designed for semantic matching through error trees.
Do not replace a producer contract with a generic nil detector
A reflection-based helper that searches arbitrary interface values for nil pointers would impose a policy the producer never declared. It could silently discard a meaningful typed value, rewrite a wrapper’s behavior, or treat unrelated error types as equivalent merely because their current dynamic value is nil.
No such general mechanism is needed here. The producer knows whether fail is false and can return the correct interface directly. If a system needs a richer boundary policy for specific error types, that policy deserves its own named contract and tests. It should not be smuggled into every consumer as a generic “nil-like” detector.
Inspect an errors.As match before dereferencing its target
The errors package documentation defines errors.As as finding the first error in the tree that matches the target, assigning that error value to the target, and returning true. In this fixture, the target is declared as var target Fault, so the call receives &target, whose type is *Fault.
The typed-nil rows demonstrate why the Boolean result and pointee state are separate evidence:
var target Fault
if errors.As(err, &target) {
if target == nil {
// The dynamic type matched Fault, but there is no Fault to dereference.
// Treat this as contract evidence, not as an approved success value.
} else {
fmt.Println(target.Code)
}
}G1, G2, G6, and G7 return true from errors.As, yet target remains nil because the matched *Fault value is nil. G3 and G5 also return true, but their targets are non-nil; each producer constructed its Fault with Code: "E_SYNTH". G0, G4, and G8 return false; their target variables remain at the initialized nil value because no match occurred.
Therefore target_nil=true has two meanings that must not be merged: “no match, target untouched” and “successful match whose assigned pointer is nil.” Record AsFault and TargetNil together. Never read target.Code merely because errors.As returned true under this contract.
For application diagnostics, the safest classification is three-state rather than Boolean: no Fault match; matched Fault with a nil target, which is a contract anomaly here; and matched non-nil Fault, which may be inspected for Code. This classification keeps errors.As useful without letting a type match authorize a dereference or redefine success.
Forward and wrap the defective success value
G6 applies forward to flawed(false):
func forward(e error) error { return e }Forwarding does not repeat the concrete-to-interface conversion; it receives an already non-nil error interface and returns that value. G6 therefore preserves G2’s dynamic type, nil target state, safe text, and ERROR branch.
G7 passes the same value to wrap. Because e != nil is true, the function constructs fmt.Errorf("operation: %w", e). The official Go error-wrapping article explains that %w gives the returned error an Unwrap method that returns the wrapped error, making it available to errors.Is and errors.As. In the observed run, %T for the outer value was *fmt.wrapError; that package-private name is recorded evidence, not a public contract to hard-code.
The outer error remains non-nil, its safe text is operation: nil fault, and errors.As traverses to the wrapped typed-nil *Fault. It returns true while the target remains nil. G8 supplies wrap with repaired(false), a literal nil interface. The guard is false, the wrapper returns nil, and success remains success.
A wrapper cannot infer the producer’s intended success
wrap obeys the value it receives. A non-nil interface tells it that an error exists, so adding context is internally consistent. The wrapper cannot infer that an upstream Boolean was false or that a nil pointer once represented intended success.
Changing only the text, suppressing a log, or using a nil-aware formatter would leave the interface non-nil. Repair the producer that first converts the typed nil. Repair a wrapper only when that wrapper itself manufactures a non-nil error from a genuinely nil input. Do not make ordinary wrappers guess around an upstream contract violation.
Read the result ledger across all boundaries
The complete manifest separates observed mechanics from control roles and release decisions:
Case | Value path | Dynamic type observed | e==nil | Is(e,nil) | As match | Target state | Text | Branch | Role / decision |
G0 | Empty error | <nil> | true | true | false | No match; remains nil | empty | SUCCESS | Nil-interface control |
G1 | Nil *Fault converted to error | *main.Fault | false | false | true | Match; assigned nil | nil fault | ERROR | Typed-nil counterexample |
G2 | flawed(false) | *main.Fault | false | false | true | Match; assigned nil | nil fault | ERROR | REPAIR producer |
G3 | flawed(true) | *main.Fault | false | false | true | Match; non-nil | E_SYNTH | ERROR | Real-failure control |
G4 | repaired(false) | <nil> | true | true | false | No match; remains nil | empty | SUCCESS | ACCEPT_BOUNDED |
G5 | repaired(true) | *main.Fault | false | false | true | Match; non-nil | E_SYNTH | ERROR | ACCEPT_BOUNDED |
G6 | Forward G2 | *main.Fault | false | false | true | Match; assigned nil | nil fault | ERROR | REPAIR upstream |
G7 | Wrap G2 | Observed *fmt.wrapError | false | false | true | Match; assigned nil | operation: nil fault | ERROR | REPAIR upstream |
G8 | Wrap G4 | <nil> | true | true | false | No match; remains nil | empty | SUCCESS | ACCEPT_BOUNDED |
All nine rows have test status PASS in the commissioning record because every observation matches its literal oracle. The row-level roles and decisions differ because a diagnostic test can faithfully reproduce a defect. G4, G5, and G8 establish the acceptable repaired contract; G2 identifies the producer path requiring repair; G3 and G5 ensure a real failure was not erased.
The wrapper’s concrete type is intentionally marked “observed.” A future implementation could use another unexported type while preserving the documented %w, Unwrap, Is, and As behavior. The release oracle should assert those behaviors, not the private type name.
Reject shortcuts that hide the original defect
Several tempting changes can make one symptom disappear without satisfying the contract.
Branching on text is invalid. Treating "nil fault" as success still leaves a non-nil interface and couples control flow to authored prose. Text can be localized, wrapped, or changed without changing the error’s type. G7 already changes it to operation: nil fault. The branch must come from the error contract, not a string exception. Changing Error to return an empty string would only make the defect quieter; G1, G2, G6, and G7 would still select ERROR.
Treating every successful errors.As match with a nil target as success also moves producer knowledge into consumers. In this fixture, that state is evidence of the typed-nil defect. It is not a universal declaration that every nil pointer carried by an error interface means “no error.” Such a rule would also make forwarding and wrapping layers responsible for guessing intent they do not possess.
Returning nil unconditionally fixes G2 only by destroying G3 and G5. Those real-failure controls require a non-nil *Fault, code E_SYNTH, matching errors.As, non-nil target, and ERROR branch. Any repair that turns them into success fails qualification.
Normalizing arbitrary errors with reflection broadens a local correction into an all-interface policy. It may suppress values from types with different documented semantics, bypass wrapper behavior, and make reviews depend on a generic detector rather than the producer’s explicit result. That approach is outside this bounded repair.
Weakening every consumer’s err != nil check conceals the source defect and creates divergent handling across call sites. The ordinary check is correct for G0, G3, G4, G5, and G8. Keep it ordinary and fix the value delivered to it.
Finally, do not turn this local value test into external integration decisions. Retries, remote schemas, credentials, and rate limits cannot explain or repair the concrete-to-interface conversion. The nine-case fixture is complete without them.
Apply an accept, repair or hold decision
Use three explicit decisions rather than one ambiguous “tests passed” label.
ACCEPT_BOUNDED applies when the identified source and environment produce the complete manifest, success returns a nil interface, and genuine failures remain observable. In this fixture, G4 shows direct repaired success, G5 shows the real failure remains non-nil with E_SYNTH, and G8 shows a conventional wrapper preserves literal nil. The acceptance is bounded to the declared contract and evidence; it is not a guarantee about every error type or Go program.
REPAIR applies to the first boundary that manufactures a non-nil success error. G2 locates that boundary at return f in flawed(false). G6 and G7 show downstream propagation, not independent evidence that forwarding or %w is broken. Patch the producer to return literal nil. If another wrapper constructs an error even when its input is genuinely nil, that wrapper owns its own repair.
HOLD applies when evidence is missing or contradictory: fewer than nine rows, a failing literal oracle, an unwritten report, an untracked source edit, an unexpected As/target combination, or an environment change accompanied by different observations. A different private wrapper type name alone is not a contradiction when documented behavior still matches. Investigate before operational approval rather than rewriting the oracle around an unexplained result.
Treat an unavailable rerun as an evidence limit, not a drafting ban
The supplied commissioning run is identified by source hash, environment, timestamps, and PASS_9_CASES. It can support a clearly attributed explanation even when another machine is unavailable. Lack of a fresh rerun limits the claim; it does not require inventing one or abandoning the article.
Conversely, edited source is a new artifact. A syntax check, code review, or prior run does not establish the modified behavior. Generate a new report with the actual environment, source identity, and complete ledger before accepting that implementation.
Recover affected behavior and assign the return-value owner
A code repair changes future returns. It does not retroactively change calls that already selected an error branch, operations that were skipped, or retries that may already have occurred. Recovery therefore needs evidence independent of the malformed error value.
For an affected operation, first identify what the application was supposed to do after the producer returned. Then inspect durable, independently recorded outcomes: committed state, an idempotency record, a completed job marker, an emitted artifact, or another domain-specific fact that exists separately from the error string. Retry only when that evidence shows the intended work did not complete and the retry policy permits it. A historical nil fault message alone cannot prove whether work happened before the bad return crossed the boundary.
Keep responsibility explicit:
Owner | Responsibility |
Producer maintainer | Construct the return value; use literal nil for success; preserve genuine Fault values for failure |
Consumer maintainer | Keep ordinary err != nil branching; avoid producer-specific nil-like exceptions |
Adapter or wrapper maintainer | Preserve nil inputs; add context only to actual non-nil errors; expose wrapped errors deliberately |
QA engineer | Test intended success, genuine failure, forwarding, wrapping, Is, As, target state, text, branch, and evidence identity |
Release reviewer | Separate test status from application acceptance; choose ACCEPT_BOUNDED, REPAIR, or HOLD |
A practical repair sequence is:
Search for functions returning error that keep a concrete pointer local and return that variable from a shared exit.
Change only confirmed no-error paths to return nil; preserve explicit non-nil errors.
Add paired success and failure regression cases before refactoring adjacent code.
Exercise forwarding and wrapping adapters so the repaired nil remains nil and real errors remain inspectable.
Record the exact source and environment for the qualifying run.
Review historical effects from independent outcome evidence rather than replaying every call that logged an error.
This ownership model complements cloud application operating discipline, but no cloud platform or deployment is required to establish the local defect. Deployment evidence answers where a change ran; this fixture answers what value crossed the Go boundary.
Recheck the adapters whenever result types or method sets change. Moving Error between value and pointer receivers, changing a function from concrete to interface return, or introducing a new wrapper can alter which conversions occur and what errors.As can assign. The regression ledger should travel with the owning package so those changes are reviewed against both success and real failure.
Strengthen software design through structured practice
Typed-nil diagnosis is a compact example of larger software-engineering habits: define an interface contract, build a reproducible fixture, separate observations from decisions, retain negative controls, and assign ownership at the boundary that constructs the value.
As verified on October 14, 2026, Refonte Learning's Software Engineering program is listed as a three-month program with a 12-14 hour weekly commitment. Its published scope includes the software development lifecycle, full-stack development, cloud computing and microservices, scalable software solutions, performance optimization, application security, and a capstone project. These are relevant foundations for practicing contract-driven implementation and evidence-based review; the page does not establish Go-specific coverage of typed nil errors.
The same discipline scales beyond this fixture: state the intended result, preserve an executable counterexample, test the repaired path and its negative control, and document who owns construction versus consumption. That is stronger than relying on a log line or a reviewer’s intuition about what a nil pointer “should” mean after conversion.
The release rule remains concrete: verify the returned interface, not only the local pointer. Success must cross the boundary as literal nil; a genuine Fault must remain a detectable error.
