Repository: getsentry/sentry-go (Go)

Issue:
panic: runtime error: hash of unhashable type mongo.CommandError
### How do you use Sentry?

Self-hosted / on-premises

### SDK version

0.36.0

### Go version

1.25.3

### Are you using Go Modules?

yes

### Steps to reproduce

Handle mongo go driver's [mongo.CommandError](https://github.com/mongodb/mongo-go-driver/blob/53057d23bc3d69cd2704f3339949310668ca9689/mongo/errors.go#L375) and send as exception to the sentry. I am not sure this bug belongs to Mongo driver or Sentry Go SDK.

```go
func main() {
     cursor, err := db.Collection("c").Find(ctx, mongo.M{})
     if err != nil {
         sentry.CurrentHub().CaptureException(err)
         return err
     }

     // reading stuff

     if err = cursor.Err(); err != nil {
         sentry.CurrentHub().CaptureException(err)
         return err
     }
}
```

### Expected result

No panic

### Actual result

Panicking at [this](https://github.com/getsentry/sentry-go/blob/2cf1dc0c1f43fad7a1b2ddba600b0d96c1681f21/exception.go#L45) line with the following trace (this trace was from our production application):

```
2025-10-19 19:41:38.181	
app/pkg/sentry.(*isentry).CaptureException(0x2aa6b77?, {0x30e87e0?, 0xc001273460?})
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/hub.go:255 +0xac
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.(*Hub).CaptureException(0xc0005bcbd0, {0x30e87e0, 0xc001273460})
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/client.go:448 +0x3d
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.(*Client).CaptureException(0xc0004e5200, {0x30e87e0?, 0xc001273460?}, 0xc003f9e5a0, {0x30ea080, 0xc00052edc0})
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/client.go:600 +0x1a5
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.(*Client).EventFromException(0xc0004e5200, {0x30e87e0, 0xc001273460}, {0x2aa7ab4, 0x5})
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/interfaces.go:425
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.(*Event).SetException(...)
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/exception.go:19 +0xf6
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.convertErrorToExceptions({0x30e87e0, 0xc001273460}, 0x64)
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/exception.go:95 +0x571
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.convertErrorDFS({0x30e87e0, 0xc001273460}, 0xc001927448, 0x0, {0x0, 0x0}, 0xc001927588, 0x64, 0x0)
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/exception.go:95 +0x571
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.convertErrorDFS({0x30e87e0, 0xc001273420}, 0xc001927448, 0xc0010988f0, {0x2aaa028, 0x6}, 0xc001927588, 0x64, 0x1)
2025-10-19 19:41:38.181	
	/home/runner/go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/exception.go:45 +0x8a
2025-10-19 19:41:38.181	
github.com/getsentry/sentry-go.convertErrorDFS({0x30ed240, 0xc0005e7a40}, 0xc001927448, 0xc0010988f8, {0x2aaa028, 0x6}, 0xc001927588, 0x64, 0x2)
2025-10-19 19:41:38.181	
goroutine 792515 [running]:
2025-10-19 19:41:38.181	

2025-10-19 19:41:38.181	
panic: runtime error: hash of unhashable type mongo.CommandError
```


Discussion:
<p><a href="https://linear.app/getsentry/issue/GO-90/panic-runtime-error-hash-of-unhashable-type-mongocommanderror">GO-90</a></p>
We are getting the same when logging our own unhashable error.

```
/usr/local/go/src/runtime/panic.go:792 +0x124
github.com/getsentry/sentry-go.convertErrorDFS({0xbb2900, 0x4000f41d70}, 0x4000cc1210, 0x4000e9ab20, {0xa5ae69, 0x6}, 0x4000cc1348, 0x64, 0x2)
	/retracted/.go/pkg/mod/github.com/getsentry/sentry-go@v0.36.0/exception.go:45 +0x60
```
Just released v0.36.1 to address this.
Thank you very much



Patch:
diff --git a/exception.go b/exception.go
index 46e8ba868..9505c9902 100644
--- a/exception.go
+++ b/exception.go
@@ -13,10 +13,40 @@ const (
 	MechanismSourceCause string = "cause"
 )
 
+type visited struct {
+	comparable map[error]struct{}
+	msgs       map[string]struct{}
+}
+
+func (v *visited) seenError(err error) bool {
+	t := reflect.TypeOf(err)
+	if t == nil {
+		return false
+	}
+
+	if t.Comparable() {
+		if _, ok := v.comparable[err]; ok {
+			return true
+		}
+		v.comparable[err] = struct{}{}
+		return false
+	}
+
+	key := t.String() + err.Error()
+	if _, ok := v.msgs[key]; ok {
+		return true
+	}
+	v.msgs[key] = struct{}{}
+	return false
+}
+
 func convertErrorToExceptions(err error, maxErrorDepth int) []Exception {
 	var exceptions []Exception
-	visited := make(map[error]bool)
-	convertErrorDFS(err, &exceptions, nil, "", visited, maxErrorDepth, 0)
+	vis := &visited{
+		make(map[error]struct{}),
+		make(map[string]struct{}),
+	}
+	convertErrorDFS(err, &exceptions, nil, "", vis, maxErrorDepth, 0)
 
 	// mechanism type is used for debugging purposes, but since we can't really distinguish the origin of who invoked
 	// captureException, we set it to nil if the error is not chained.
@@ -37,15 +67,14 @@ func convertErrorToExceptions(err error, maxErrorDepth int) []Exception {
 	return exceptions
 }
 
-func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited map[error]bool, maxErrorDepth int, currentDepth int) {
+func convertErrorDFS(err error, exceptions *[]Exception, parentID *int, source string, visited *visited, maxErrorDepth int, currentDepth int) {
 	if err == nil {
 		return
 	}
 
-	if visited[err] {
+	if visited.seenError(err) {
 		return
 	}
-	visited[err] = true
 
 	_, isExceptionGroup := err.(interface{ Unwrap() []error })
 


Test patch:
diff --git a/exception_test.go b/exception_test.go
index e8970f9ff..a8a5743a3 100644
--- a/exception_test.go
+++ b/exception_test.go
@@ -377,3 +377,21 @@ func TestCircularReferenceProtection(t *testing.T) {
 		})
 	}
 }
+
+// unhashableSliceError is a non-comparable error type.
+type unhashableSliceError []string
+
+func (e unhashableSliceError) Error() string {
+	return "unhashable slice error"
+}
+
+func TestConvertErrorToExceptions_UnhashableError_NoPanic(t *testing.T) {
+	defer func() {
+		if r := recover(); r != nil {
+			t.Fatalf("convertErrorToExceptions panicked for unhashable error: %v", r)
+		}
+	}()
+
+	err := unhashableSliceError{"a", "b"}
+	_ = convertErrorToExceptions(err, -1)
+}