go-migrationGo Migration
Error Handling
Documentation

Error Handling

Handle and diagnose errors from go-migration using typed error values, errors.Is(), errors.As(), and a troubleshooting guide for common scenarios.

Error Handling

go-migration returns typed error values for common failure scenarios. This lets you inspect errors programmatically using Go's standard errors.Is() and errors.As() functions, rather than relying on string matching.

Typed Error Values

go-migration exports the following sentinel errors. The migrator package re-defines the full set used across the system, while the database and seeder packages export the subset relevant to their domain:

ErrorPackage(s)Description
ErrMigrationNotFoundmigratorThe requested migration name does not exist in the registry
ErrDuplicateMigrationmigratorA migration with the same name has already been registered
ErrInvalidMigrationNamemigratorA migration name does not match the expected format
ErrTrackingTablemigratorThe migration tracking table could not be read or written
ErrTransactionFailedmigrator, databaseA transaction (begin, commit, or rollback) failed
ErrConnectionFailedmigrator, databaseThe database connection could not be established
ErrConnectionNotFoundmigrator, databaseA named connection does not exist
ErrUnsupportedTypemigratorA column type is not supported by the active grammar
ErrConfigValidationmigratorConfiguration validation failed
ErrDriverNotFounddatabaseNo driver is registered for the given name
ErrNoDefaultdatabaseNo default connection has been set
ErrSeederNotFoundmigrator, seederThe requested seeder name does not exist in the registry
ErrDuplicateSeedermigrator, seederA seeder with the same name has already been registered
ErrInvalidSeederNamemigrator, seederA seeder name does not match the expected format
ErrCircularDependencymigrator, seederSeeder dependencies form a cycle that cannot be resolved

These errors are defined in the migrator, database, and seeder packages:

go
import (
    "github.com/gopackx/go-migration/pkg/database"
    "github.com/gopackx/go-migration/pkg/migrator"
    "github.com/gopackx/go-migration/pkg/seeder"
)

Using errors.Is()

Use errors.Is() to check whether an error matches a specific typed error. This works even when the error is wrapped with additional context.

main.go
package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"

    _ "github.com/lib/pq"

    "github.com/gopackx/go-migration/pkg/migrator"
)

func main() {
    db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    m := migrator.New(db)

    // Register migrations...

    if err := m.Up(); err != nil {
        switch {
        case errors.Is(err, migrator.ErrDuplicateMigration):
            log.Println("A migration with the same name is already registered")
        case errors.Is(err, migrator.ErrMigrationNotFound):
            log.Println("A referenced migration is not registered")
        case errors.Is(err, migrator.ErrTransactionFailed):
            log.Printf("Migration transaction failed: %v", err)
        case errors.Is(err, migrator.ErrConnectionFailed):
            log.Println("Could not connect to the database")
        default:
            log.Fatalf("Unexpected error: %v", err)
        }
    }

    fmt.Println("Migrations applied successfully")
}

Checking Rollback Errors

main.go
if err := m.Rollback(0); err != nil {
    if errors.Is(err, migrator.ErrTransactionFailed) {
        log.Printf("Rollback transaction failed: %v", err)
        // Investigate the database state manually
    } else if errors.Is(err, migrator.ErrMigrationNotFound) {
        log.Println("A migration in the batch is no longer registered")
    } else {
        log.Fatalf("Unexpected rollback error: %v", err)
    }
}

Checking Seeder Errors

main.go
import (
    "errors"
    "log"

    "github.com/gopackx/go-migration/pkg/seeder"
)

if err := runner.RunAll(); err != nil {
    switch {
    case errors.Is(err, seeder.ErrSeederNotFound):
        log.Println("Seeder not found in the registry")
    case errors.Is(err, seeder.ErrCircularDependency):
        log.Println("Circular dependency detected between seeders")
    default:
        log.Fatalf("Seeding failed: %v", err)
    }
}

Using errors.As()

Use errors.As() to extract a specific error type and access its fields. This is useful when go-migration wraps errors with additional context like the migration name or underlying database error.

main.go
package main

import (
    "database/sql"
    "errors"
    "log"

    _ "github.com/lib/pq"

    "github.com/gopackx/go-migration/pkg/migrator"
)

func main() {
    db, err := sql.Open("postgres", "postgres://user:password@localhost:5432/mydb?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    m := migrator.New(db)

    // Register migrations...

    if err := m.Up(); err != nil {
        var migrationErr *migrator.MigrationError
        if errors.As(err, &migrationErr) {
            log.Printf("Migration '%s' failed: %v", migrationErr.MigrationName, migrationErr.Cause)
            log.Printf("Failing SQL: %s", migrationErr.SQL)
            if migrationErr.Position != "" {
                log.Printf("Position: %s", migrationErr.Position)
            }
        } else {
            log.Fatalf("Error: %v", err)
        }
    }
}

The MigrationError type provides structured information about the failure:

go
type MigrationError struct {
    MigrationName string // Name of the migration that failed
    SQL           string // The SQL statement that triggered the failure
    Position      string // Position information from the DB (optional)
    Cause         error  // The underlying error from the database driver
}

errors.As() unwraps the error chain automatically. Even if the error is wrapped multiple times, errors.As() will find the first matching type in the chain.

Wrapping Errors in Migrations

When returning errors from your Up or Down methods, wrap them with fmt.Errorf and the %w verb to preserve the error chain:

migrations/create_users_table.go
func (m *CreateUsersTable) Up(s *schema.Builder) error {
    err := s.Create("users", func(bp *schema.Blueprint) {
        bp.ID()
        bp.String("email", 255).Unique()
        bp.Timestamp("created_at")
    })
    if err != nil {
        return fmt.Errorf("failed to create users table: %w", err)
    }
    return nil
}

This ensures callers can use both errors.Is() and errors.As() to inspect the full error chain.

Troubleshooting

Common error scenarios you may encounter when using go-migration:

ScenarioCauseSolution
"tracking table" errorsThe migrations tracking table could not be created or readEnsure the database user has DDL privileges; m.Up() creates the tracking table automatically when it does not exist
"duplicate migration name"Two migrations registered with the same name via m.Register()Ensure each migration has a unique timestamp-prefixed name (e.g., 20240101000001_create_users_table)
"migration not found"Calling m.Rollback() or referencing a migration name that isn't registeredVerify the migration is registered with m.Register() before running operations
"database connection refused"The database server is not running or the connection string is incorrectCheck that the database is running, verify host/port/credentials, and test with db.Ping()
"transaction deadlock"Two concurrent migrations or queries are waiting on each other's locksAvoid running migrations concurrently; use DisableTransaction() for long-running DDL operations that may conflict
"permission denied"The database user lacks privileges to create/alter/drop tablesGrant the necessary DDL privileges (CREATE, ALTER, DROP) to the database user
"rollback failed"The Down method contains an error or references objects that don't existReview the Down method logic; ensure it reverses the Up method correctly and handles missing objects gracefully
"circular dependency"Seeder A depends on Seeder B, which depends on Seeder A (directly or transitively)Restructure seeder dependencies to form a directed acyclic graph (DAG) — remove or reorganize the cycle
"seeder not found"Calling runner.Run("Name") with a seeder name that isn't registeredCheck the seeder name matches exactly what was passed to runner.Register()
"connection pool exhausted"Too many concurrent operations exceed MaxOpenConnsIncrease MaxOpenConns in your configuration or reduce concurrent database operations
"SSL certificate error"PostgreSQL sslmode is set to verify-ca or verify-full but the certificate is missing or invalidProvide valid SSL certificates or set sslmode: disable for local development
"column type not supported"Using a column type that the current database grammar doesn't supportCheck the Database Grammars page for supported types per database engine

Always test migrations in a staging environment before running them in production. Use m.Status() to review pending migrations and m.Rollback(0) to undo the last batch if something goes wrong.

Best Practices

  • Always check errors — never discard the return value from m.Up(), m.Rollback(), or seeder operations
  • Use errors.Is() for sentinel errors — check for specific error types to handle known failure modes gracefully
  • Use errors.As() for structured errors — extract the migration name, failing SQL, and underlying cause from MigrationError for detailed logging
  • Wrap errors with %w — preserve the error chain in your migration Up and Down methods so callers can inspect the full context
  • Log before exiting — when a migration fails, log the error details before terminating to aid debugging

What's Next?

  • Package Reference — complete method signatures for all go-migration packages
  • CLI Reference — run migrations from the command line
  • Hooks — execute custom logic before and after migrations