-
Type:
Task
-
Resolution: Unresolved
-
Priority:
Unknown
-
None
-
Affects Version/s: None
-
Component/s: Error Handling
-
None
The MongoDB server publicly documents its error codes here, along with a more complete but less accessible list here. These codes are helpful for detecting and handling certain server states like index conflicts, transaction timeouts, failovers, or other transient issues. For the most part, it's expected that users understand these errors and do categorization themselves if needed (e.g. define a set of retryable errors for which to do exponential backoff).
However if you're working with the Go driver, it's not very straightforward to do this categorization, partially due to the error types and partially due to the documentation around those types.
If you were to look at the driver's error types for the first time, you might start writing a helper function like this to extract a server error code:
func getMongoErrorCode(err error) int { var driverError driver.Error if errors.As(err, &driverError) { return driverError.Code } var bulkWriteError mongo.BulkWriteError if errors.As(err, &bulkWriteError) { return bulkWriteError.Code } ... repeat for CommandError, WriteError, WriteConcernError, etc }
And then use this helper to build a set of error codes that you're interested in, e.g.
func IsMongoTransactionError(err error) bool { switch getMongoErrorCode(err) { case 225, 251, 257, ... return true default: return false }
From my recollection of working at Mongo, some internal tools like ADL, mongomirror and mongosync do exactly this. But this approach is pretty clunky, presents risk as an error type can get missed, and requires writing loops for composite error types like WriteException. It also feels unintuitive to be writing code like this that supplements the driver.
It looks like there's a ServerError interface that, if the user notices it, captures all of these error types and can be optionally be wrapped like so:
func hasMongoErrCode(err error, code int) bool { var serverError mongo.ServerError if errors.As(err, &serverError) { return serverErr.HasErrorCode(code) } return false }
And then be used like this to build a set of error codes:
func IsMongoTransactionError(err error) bool {
return hasMongoErrCode(err, 225) ||
hasMongoErrCode(err, 251) ||
hasMongoErrCode(err, 257) ||
...
This is pretty much how the IsDuplicateKeyError driver helper is written, but still feels pretty clunky and non-idiomatic as you're doing reflection for every error code in the set. You also can't use a switch statement or create a map with these codes as easily.
My minimum suggestions would be to:
- Add a GetErrorCode() method to the ServerError interface. This helps avoid reflection for each error code that you're checking. It seems like this should already exist given HasErrorCode().
- Update the docstrings in errors.go to guide readers to use the ServerError interface. This helps avoid the "internal" error types like WriteError. From my experience, it's better to avoid these because it's not always clear what kind of error will be returned from a driver call.
- For example, if I do InsertMany with one doc, can that give me a BulkWriteError because it's InsertMany, a WriteError because it's one doc, or a CommandError because I'm issuing a command? Does this depend on the Mongo server version? And so on.
I might also suggest separating errors.go into multiple files that split the logic into "high level" vs "low level" usage. A high level file could have the ServerError interface and helpers like IsDuplicateKeyError, while a low level file could have the other errors like WriteError and come with a disclaimer that you should only be using these directly for good reason.
Also happy to discuss further – thank you!