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. Each error is defined once, in the package that owns it; the migrator package re-exports the database, seeder, and grammar errors as aliases of the same values, so errors.Is(err, migrator.ErrSeederNotFound) and errors.Is(err, seeder.ErrSeederNotFound) are equivalent:

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 created, read, or written. The message includes the driver's error (migration tracking table error: <cause>), and the cause stays in the error chain
ErrLockTimeoutmigratorThe cross-process migration lock could not be acquired within the lock timeout, usually because another process is running migrations or seeders (new in v1.2.0)
ErrTransactionFailedmigrator, databaseA transaction (begin, commit, or rollback) failed
ErrConnectionFailedmigrator, databaseThe database connection could not be established
ErrConnectionNotFoundmigrator, databaseA named connection does not exist
ErrUnsupportedTypemigrator, grammarsA column type is not supported by the active grammar
ErrConfigValidationconfigConfiguration validation failed. No longer re-exported by migrator since v1.2.0
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, seeder, config, and schema/grammars packages:

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

Removed in v1.2.0: migrator.ErrConfigValidation. The migrator never returned it — it only aliased the config package's error. Use config.ErrConfigValidation instead, so the migrator library no longer depends on the configuration loader.

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"

    "github.com/gopackx/go-migration/pkg/schema/grammars"
)

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, migrator.WithGrammar(grammars.NewPostgresGrammar()))

    // 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 Lock Timeouts

Operations that change the database wait for the migration lock up to the lock timeout (30s by default, set with migrator.WithLockTimeout). When it elapses, the operation fails with an error wrapping migrator.ErrLockTimeout and nothing has been changed:

main.go
if err := m.Up(); err != nil {
    if errors.Is(err, migrator.ErrLockTimeout) {
        // Another process holds the lock; it is safe to retry later.
        log.Printf("migrations are running elsewhere: %v", err)
        os.Exit(1)
    }
    log.Fatal(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"

    "github.com/gopackx/go-migration/pkg/schema/grammars"
)

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, migrator.WithGrammar(grammars.NewPostgresGrammar()))

    // 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
"migration tracking table error: …"The migrations tracking table could not be created or read; the text after the colon is the driver's errorRead the driver cause. Ensure the database user has DDL privileges (m.Up() creates the table automatically), that a schema-qualified migration_table points at an existing schema, and that you pass WithGrammar for MySQL/SQLite
"dry-run: migration tracking table … does not exist; run migrate:install first"Dry-run never creates the tracking tableRun migrate:install once (or migrator.NewTrackerWithGrammar(db, table, grammar).EnsureTable()) before using --dry-run / WithDryRun
"could not acquire migration lock within 30s"Another process holds the migration lock (ErrLockTimeout)Wait for the other process, raise --lock-timeout / WithLockTimeout, or check for a stuck process holding the lock
"migration lock: the connection pool is limited to 1 open connection"Locking is enabled on PostgreSQL/MySQL with MaxOpenConns = 1Set max_open_conns to 0 or at least 2, or disable locking with --no-lock / WithoutLock()
"invalid description: … must be lower snake_case"A make:migration, make:seeder, or make:factory name is not lower snake_caseUse a name matching ^[a-z][a-z0-9_]*$, e.g. create_orders_table
"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"A migration and application queries are waiting on each other's locksConcurrent migration runs are already serialised by the migration lock; for long-running DDL that conflicts with application traffic, run it in a quieter window or use DisableTransaction()
"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 reg.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