Dolt is a SQL database with Git-style version control. Under the hood, Dolt’s SQL engine is powered by go-mysql-server, a MySQL-compatible query engine written in Go.
Dolt and go-mysql-server have various users with different needs when providing actionable feedback. A developer needs debugging context. A SQL client needs a stable error code. A CLI user needs a clean message and sometimes a hint about what to do next. In a system failure, how we do we always communicate all of these effectively?
What is an error in Go?#
Well before that, let’s get familiar with the Go error. In Go, an error is a value. A function either succeeds and returns results, or it fails and returns an error value. As a result, error is an interface, meaning it can be any type that has an Error() string method.
func foo(s string) error {
if s == "" {
return errors.New("input cannot be empty")
}
return nil
}
This simple style is great for smaller programs, but databases are big. We’re dealing with multiple subsystems: users, software (compatibility with client and tools), and engineers. So Dolt and go-mysql-server use multiple error types for these different situations.
We write about debugging errors separately. Stack traces in Go have a bit more to offer than what we could cover here.
The Kind error#
A SQL engine has many error cases — just take a look at this excerpt of the MySQL error server reference. In order to offer similar errors and organization, go-mysql-server uses “kinds” to represent all distinct error categories. A Kind is a reusable template for building errors:
import "gopkg.in/src-d/go-errors.v1"
var (
ErrTableNotFound = errors.NewKind("table %s not found")
)
func openTable(name string) error {
return ErrTableNotFound.New(name)
}
func handle(err error) {
if ErrTableNotFound.Is(err) {
// fallback path
}
}
These are more difficult to break on accident when compared to literal string values. It also serves as a quality of life improvement, as all error instantiations of this appear from a single easily trackable origin. In fact, that origin is our focus, because error values have to be translated (SQL engine anatomy).
The MySQL Boundary#
Alright, we understand the error interface and Kind construct, but these are all Go errors. Where are the actual MySQL error codes from the reference above?
MySQL clients typically expect certain failures to be represented with specific numeric codes, and sometimes SQLSTATE values. A plain Go error cannot satisfy this. go-mysql-server uses our own fork of Vitess to provide this extra metadata. In Vitess, a mysql.SQLError includes the fields we’ve covered. go-mysql-server provides a conversion step that maps our internal kinded errors to these protocol errors in sql/errors.go.
func CastSQLError(err error) mysql.SQLError {
//...
switch {
case ErrTableNotFound.Is(err):
code = mysql.ERNoSuchTable
case ErrDatabaseExists.Is(err):
code = mysql.ERDbCreateExists
//...
default:
code = mysql.ERUnknownError
}
return mysql.NewSQLError(code, sqlState, "%s", err.Error())
}
So internally, expect these Kind errors for go-mysql-server, but remember their main purpose is as a conversion point for Vitess and the MySQL protocol. You can also see why we care about the stability of these types, especially after we learned Sentinel errors and errors.Is() slow your code down by 500%.
There’s more?#
Yes, there is more. These kinded-like error patterns happen in Dolt’s core (storage and version control) logic. One example is plain sentinel errors, which are reusable values like “branch not found” without the extra metadata. Or custom structs that carry Dolt-specific metadata, such as a version number.
As a quick example, take a look at Dolt’s CLI, where users typically expect usage feedback. VerboseError and DError (a builder API) can bundle an extra display message, optional details and an optional cause chain.
func runCommand() errhand.VerboseError {
cause := ErrTableNotFound.New("plants") // usually returned by go-mysql-server
return errhand.BuildDError("Could not run query.")
.AddDetails("The table name must exist in the current database")
.AddCause(cause)
.Build()
}
Conclusion#
Dolt and go-mysql-server have multiple error types because, errors do more than say “something is wrong”. They carry meaning internally, for subsystems, and users, stacking in various ways depending on the context. We use special patterns like sentinels, kinded errors, and custom structs to keep things organized.
Hopefully this gives you the needed context for error handling to start contributing to Dolt! If you have any further questions on this topic or Dolt in general, make sure to join the Discord.
