Static Analysis for Go Error Type Consistency: errortype Linter

196 views
Skip to first unread message

Oliver Eikemeier

unread,
Aug 15, 2025, 7:10:42 AMAug 15
to golang-nuts

Static Analysis for Go Error Type Consistency: errortype Linter

I've been investigating a class of subtle bugs in Go error handling related to pointer vs. value semantics with errors.As, and developed a static analysis tool to detect these issues. I'm seeking feedback from the community on both the approach and the tool's effectiveness.

Problem Statement

Consider this code attempting to handle AES key size errors:

	key := []byte("My kung fu is better than yours")
	_, err := aes.NewCipher(key)

	var kse *aes.KeySizeError
	if errors.As(err, &kse) {
		fmt.Printf("AES keys must be 16, 24 or 32 bytes long, got %d bytes.\n", kse)
	} else if err != nil {
		fmt.Println(err)
	}

The bug: aes.NewCipher returns aes.KeySizeError as a value, but the code checks for a pointer to *aes.KeySizeError. This type mismatch causes errors.As to fail silently, with no compile-time detection.

Analysis and Solution Approach

The core issue is that Go's error interface allows both pointer and value types to implement error, but the dynamic type matching in errors.As requires exact type correspondence. This creates opportunities for silent failures when the expected and actual types don't align.

I propose a two-part mitigation strategy:

1. Compile-time Intent Declaration

Explicit compile-time assertions to document intended usage patterns:

// MyValueError is intended to be used as a value
var _ error = MyValueError{}

// MyPointerError is intended to be used as a pointer
var _ error = (*MyPointerError)(nil)

2. Static Analysis Tool

I've developed errortype, a static analyzer that detects inconsistencies between intended error type usage and actual usage patterns. The tool analyzes:

  • Function return value types
  • Type assertions and type switches
  • errors.As target parameters
  • Method receiver consistency patterns

Example diagnostic output:

main.go:14:20: Target for value error "crypto/aes.KeySizeError" is a pointer-to-pointer, use a pointer to a value instead: "var kse aes.KeySizeError; ... errors.As(err, &kse)". (et:err)

Request for Feedback

I'm particularly interested in feedback on a few points:

  1. Prevalence: Have you encountered this pointer/value ambiguity with error types? How common do you think this class of bug is in practice?

  2. Solution Approach: What are your thoughts on using var _ error = ... assertions to declare an error type's intended usage? Is this a practical convention to adopt?

  3. Tooling: The detection heuristics are based on these assertions and usage pattern analysis. I would be grateful for any real-world testing on your codebases to validate their effectiveness and performance.

The tool is currently CLI-only while I refine the detection logic based on real-world usage patterns. Integration is planned in a later phase.

References

I welcome any insights, particularly from those who have encountered similar issues or have thoughts on static analysis approaches for Go error handling patterns.

Best regards, Oliver

Kevin Chowski

unread,
Aug 18, 2025, 6:22:49 PMAug 18
to golang-nuts
Regarding the "var _ error" bit - why not just look at the Error method to see whether it is on a T or a *T? I haven't done a survey about it or anything, but if I need to know whether to pass a T or a *T I often check that type's methods.

Axel Wagner

unread,
Aug 19, 2025, 12:58:57 AMAug 19
to Oliver Eikemeier, golang-nuts
Hi,

On Fri, 15 Aug 2025 at 13:10, Oliver Eikemeier <eike...@fillmore-labs.com> wrote:

Static Analysis for Go Error Type Consistency: errortype Linter

I've been investigating a class of subtle bugs in Go error handling related to pointer vs. value semantics with errors.As, and developed a static analysis tool to detect these issues. I'm seeking feedback from the community on both the approach and the tool's effectiveness.

Problem Statement

Consider this code attempting to handle AES key size errors:

	key := []byte("My kung fu is better than yours")
	_, err := aes.NewCipher(key)

	var kse *aes.KeySizeError
	if errors.As(err, &kse) {
		fmt.Printf("AES keys must be 16, 24 or 32 bytes long, got %d bytes.\n", kse)
	} else if err != nil {
		fmt.Println(err)
	}

The bug: aes.NewCipher returns aes.KeySizeError as a value, but the code checks for a pointer to *aes.KeySizeError. This type mismatch causes errors.As to fail silently, with no compile-time detection.

Analysis and Solution Approach

The core issue is that Go's error interface allows both pointer and value types to implement error, but the dynamic type matching in errors.As requires exact type correspondence. This creates opportunities for silent failures when the expected and actual types don't align.

That is the reason why it is generally recommended that error types use a pointer receiver. That statically rules out mistakes like this. It is unfortunate, that crypto/aes did not follow this recommendation.
 

I propose a two-part mitigation strategy:

1. Compile-time Intent Declaration

Explicit compile-time assertions to document intended usage patterns:

// MyValueError is intended to be used as a value
var _ error = MyValueError{}

// MyPointerError is intended to be used as a pointer
var _ error = (*MyPointerError)(nil)

I think if someone does this, they are aware of the problem and should just make their Error methods a pointer receiver. In my opinion, that is a much better recommendation, as it means this mistake can't happen to users of a package, even if they don't use your linter.

I know that you disagree with the blanket "all error receivers should be pointers" advice, though.

2. Static Analysis Tool

I've developed errortype, a static analyzer that detects inconsistencies between intended error type usage and actual usage patterns. The tool analyzes:

  • Function return value types
  • Type assertions and type switches
  • errors.As target parameters
  • Method receiver consistency patterns
For clarity: I understand your linter to
1. Check that a package declaring SomeError uses that consistently - that is, either exclusively assigns SomeError or exclusively assigns *SomeError to error. It complains, if not.
2. If that usage is consistent, checks that any users of that package *also* only assign the type consistent with that. It complains, if not.
3. Checks that all type-assertions/-switches and errors.As/errors.Is calls happen consistent with that (if it is unambiguous)
In particular, AIUI you do not rely on the assignment-statements above to work, right?

That seems like a very useful tool.
 

Example diagnostic output:

main.go:14:20: Target for value error "crypto/aes.KeySizeError" is a pointer-to-pointer, use a pointer to a value instead: "var kse aes.KeySizeError; ... errors.As(err, &kse)". (et:err)

Request for Feedback

I'm particularly interested in feedback on a few points:

  1. Prevalence: Have you encountered this pointer/value ambiguity with error types? How common do you think this class of bug is in practice?

  2. Solution Approach: What are your thoughts on using var _ error = ... assertions to declare an error type's intended usage? Is this a practical convention to adopt?

  3. Tooling: The detection heuristics are based on these assertions and usage pattern analysis. I would be grateful for any real-world testing on your codebases to validate their effectiveness and performance.

The tool is currently CLI-only while I refine the detection logic based on real-world usage patterns. Integration is planned in a later phase.

References

I welcome any insights, particularly from those who have encountered similar issues or have thoughts on static analysis approaches for Go error handling patterns.

Best regards, Oliver

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/0AF06538-3EAB-4E89-8F9A-25B7002237B8%40fillmore-labs.com.

Oliver Eikemeier

unread,
Aug 21, 2025, 6:58:54 AMAug 21
to Kevin Chowski, golang-nuts
Am 19.08.2025 um 00:22 schrieb Kevin Chowski <Ke...@chowski.com>:

Regarding the "var _ error" bit - why not just look at the Error method to see whether it is on a T or a *T? I haven't done a survey about it or anything, but if I need to know whether to pass a T or a *T I often check that type's methods.

Unfortunately looking at receiver types does not work, lots of errors have a value receiver but are meant as pointer values.

The linter is meant to work with existing codebases without API changes, improving the status quo. Adding a “var _ error” is basically free and no API change.

Oliver Eikemeier

unread,
Aug 21, 2025, 7:24:50 AMAug 21
to Axel Wagner, golang-nuts
Hi,

Am 19.08.2025 um 06:57 schrieb Axel Wagner <axel.wa...@googlemail.com>:

Hi,

On Fri, 15 Aug 2025 at 13:10, Oliver Eikemeier <eike...@fillmore-labs.com> wrote:

Static Analysis for Go Error Type Consistency: errortype Linter

I've been investigating a class of subtle bugs in Go error handling related to pointer vs. value semantics with errors.As, and developed a static analysis tool to detect these issues. I'm seeking feedback from the community on both the approach and the tool's effectiveness.

Problem Statement

Consider this code attempting to handle AES key size errors:

	key := []byte("My kung fu is better than yours")
	_, err := aes.NewCipher(key)

	var kse *aes.KeySizeError
	if errors.As(err, &kse) {
		fmt.Printf("AES keys must be 16, 24 or 32 bytes long, got %d bytes.\n", kse)
	} else if err != nil {
		fmt.Println(err)
	}

The bug: aes.NewCipher returns aes.KeySizeError as a value, but the code checks for a pointer to *aes.KeySizeError. This type mismatch causes errors.As to fail silently, with no compile-time detection.

Analysis and Solution Approach

The core issue is that Go's error interface allows both pointer and value types to implement error, but the dynamic type matching in errors.As requires exact type correspondence. This creates opportunities for silent failures when the expected and actual types don't align.

That is the reason why it is generally recommended that error types use a pointer receiver. That statically rules out mistakes like this. It is unfortunate, that crypto/aes did not follow this recommendation.

Your opinion is actually cited as Source 9 in the accompanying blog article.

But, as mentioned, there are lots of existing codebases out there (even in the Go standard library) that use value receivers.

I propose a two-part mitigation strategy:

1. Compile-time Intent Declaration

Explicit compile-time assertions to document intended usage patterns:

// MyValueError is intended to be used as a value
var _ error = MyValueError{}

// MyPointerError is intended to be used as a pointer
var _ error = (*MyPointerError)(nil)

I think if someone does this, they are aware of the problem and should just make their Error methods a pointer receiver.

This would be an API change.

In my opinion, that is a much better recommendation, as it means this mistake can't happen to users of a package, even if they don't use your linter.

Maybe. As mentioned: Good error type design is out of scope. While errortype promotes a consistent style, a broader refactor of your error handling strategy may sometimes be the better solution.

The point of the linter is not to guide you to a redesign, but to deal with an existing codebase.

I know that you disagree with the blanket "all error receivers should be pointers" advice, though.

In the Go standard library errors that aren’t structs mostly use value receivers. For new designs I recommended following “Sensible Defaults” — mostly pointer receivers for non-empty structs. So we’re not that far apart.

My point is that we have existing code bases we can not all redesign and they have existing bugs. I want to improve that situation.

2. Static Analysis Tool

I've developed errortype, a static analyzer that detects inconsistencies between intended error type usage and actual usage patterns. The tool analyzes:

  • Function return value types
  • Type assertions and type switches
  • errors.As target parameters
  • Method receiver consistency patterns
For clarity: I understand your linter to
1. Check that a package declaring SomeError uses that consistently - that is, either exclusively assigns SomeError or exclusively assigns *SomeError to error. It complains, if not.

If you lint that package, yes. If you just use that package, it does check, but does not complain.

2. If that usage is consistent, checks that any users of that package *also* only assign the type consistent with that. It complains, if not.

For linted packages, yes. Also, if you use an error that has unclear usage in the defining package, it might complain.

3. Checks that all type-assertions/-switches and errors.As/errors.Is calls happen consistent with that (if it is unambiguous)

Yes, and also notice if the usage is ambiguous.

In particular, AIUI you do not rely on the assignment-statements above to work, right?

No, because that’s not feasible with current code bases. It would be nice when we were there, but we aren’t.

I tested the heuristic on >600 open source projects, and it mostly works.

The linter (and the heuristic) is experimental though, because we have no definite criteria. I think the linter is useful as-is, but my hope is to find some semi-official criteria which is more deterministic. That’s why I post here.

I think “var _ error” is good, but maybe there are better ideas.

That seems like a very useful tool.

Thanks. I strive for the tool to be useful, but also like input how to improve the situation in a mostly non-destructive way, meaning not discussion Go 2 errors, but how to deal with what we have.

Example diagnostic output:

main.go:14:20: Target for value error "crypto/aes.KeySizeError" is a pointer-to-pointer, use a pointer to a value instead: "var kse aes.KeySizeError; ... errors.As(err, &kse)". (et:err)

Request for Feedback

I'm particularly interested in feedback on a few points:

  1. Prevalence: Have you encountered this pointer/value ambiguity with error types? How common do you think this class of bug is in practice?
  2. Solution Approach: What are your thoughts on using var _ error = ... assertions to declare an error type's intended usage? Is this a practical convention to adopt?
  3. Tooling: The detection heuristics are based on these assertions and usage pattern analysis. I would be grateful for any real-world testing on your codebases to validate their effectiveness and performance.

The tool is currently CLI-only while I refine the detection logic based on real-world usage patterns. Integration is planned in a later phase.

References

I welcome any insights, particularly from those who have encountered similar issues or have thoughts on static analysis approaches for Go error handling patterns.

Best regards, Oliver

Thanks for the feedback,

Cheers
Oliver

Reply all
Reply to author
Forward
0 new messages