golang-migrate/migrate · warning

can't unlock, as not currently locked

Error message

can't unlock, as not currently locked

What it means

database.ErrNotLocked ("can't unlock, as not currently locked") is returned by drivers' Unlock() when the driver's isLocked flag is false — i.e. Unlock was called without a prior successful Lock, or the lock was already released. It guards the symmetry of the Lock/Unlock protocol.

Source

Thrown at database/driver.go:17

// Package database provides the Driver interface.
// All database drivers must implement this interface, register themselves,
// optionally provide a `WithInstance` function and pass the tests
// in package database/testing.
package database

import (
	"fmt"
	"io"
	"sync"

	iurl "github.com/golang-migrate/migrate/v4/internal/url"
)

var (
	ErrLocked    = fmt.Errorf("can't acquire lock")
	ErrNotLocked = fmt.Errorf("can't unlock, as not currently locked")
)

const NilVersion int = -1

var driversMu sync.RWMutex
var drivers = make(map[string]Driver)

// Driver is the interface every database driver must implement.
//
// How to implement a database driver?
//  1. Implement this interface.
//  2. Optionally, add a function named `WithInstance`.
//     This function should accept an existing DB instance and a Config{} struct
//     and return a driver instance.
//  3. Add a test that calls database/testing.go:Test()
//  4. Add own tests for Open(), WithInstance() (when provided) and Close().
//     All other functions are tested by tests in database/testing.
//     Saves you some time and makes sure all database drivers behave the same way.

View on GitHub (pinned to 01a9643f14)

Solutions

  1. Only call Unlock when Lock returned nil (check err before deferring, or defer a closure that tracks lock success).
  2. Remove duplicate Unlock calls in retry/cleanup paths.
  3. If you cannot trace the double unlock, wrap Unlock and treat errors.Is(err, database.ErrNotLocked) as benign and log instead of failing.

Example fix

// before
if err := m.Lock(); err != nil { return err } // then later in cleanup: m.Unlock() runs even on this path
// after
locked := false
if err := m.Lock(); err != nil { return err }
locked = true
defer func() { if locked { m.Unlock() } }()
Defensive patterns

Strategy: type-guard

Validate before calling

if !isLocked {
    return fmt.Errorf("cannot unlock: lock was never acquired")
}

Type guard

func canUnlock(m *migrate.Migrate, locked bool) bool { return locked }

Try / catch

if err := m.Unlock(); err != nil {
    if errors.Is(err, database.ErrNotLocked) {
        log.Println("unlock skipped: not locked")
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling m.Unlock() twice; calling Unlock on a freshly created Migrate/driver instance; a failed Lock() followed by an unconditional deferred Unlock.

Common situations: Deferred Unlock running after a Lock error; double-release in retry logic; sharing a driver across goroutines where one already unlocked.

Related errors


AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02). Data as JSON: /api/errors/3470cb6ba46fd92d. Report an issue: GitHub.