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
- Only call Unlock when Lock returned nil (check err before deferring, or defer a closure that tracks lock success).
- Remove duplicate Unlock calls in retry/cleanup paths.
- 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
- Track lock ownership with a boolean and only unlock when you locked.
- Never call Unlock after a failed Lock attempt.
- Avoid duplicate cleanup paths that both unlock.
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
- can't acquire lock
- unable to obtain lock
- unable to release already released lock
- database is dirty
- database is dirty
AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02).
Data as JSON: /api/errors/3470cb6ba46fd92d.
Report an issue: GitHub.