jackc/pgx · warning
failed to remove large object
Error message
failed to remove large object
What it means
Returned by LargeObjects.Unlink when SELECT lo_unlink($1) scanned a result that is not 1 (large_objects.go:56-58). lo_unlink returns 1 on success and 0 when the large object did not exist (or could not be removed), so this error means the OID was not found / not removable. A privilege or transaction error would instead surface as the query error from QueryRow before this check.
Source
Thrown at large_objects.go:57
func (o *LargeObjects) Open(ctx context.Context, oid uint32, mode LargeObjectMode) (*LargeObject, error) {
var fd int32
err := o.tx.QueryRow(ctx, "select lo_open($1, $2)", oid, mode).Scan(&fd)
if err != nil {
return nil, err
}
return &LargeObject{fd: fd, tx: o.tx, ctx: ctx}, nil
}
// Unlink removes a large object from the database.
func (o *LargeObjects) Unlink(ctx context.Context, oid uint32) error {
var result int32
err := o.tx.QueryRow(ctx, "select lo_unlink($1)", oid).Scan(&result)
if err != nil {
return err
}
if result != 1 {
return errors.New("failed to remove large object")
}
return nil
}
// A LargeObject is a large object stored on the server. It is only valid within the transaction that it was initialized
// in. It uses the context it was initialized with for all operations. It implements these interfaces:
//
// io.Writer
// io.Reader
// io.Seeker
// io.Closer
type LargeObject struct {
ctx context.Context
tx Tx
fd int32
}
View on GitHub (pinned to ec1a0befd2)
Solutions
- Treat this as 'not found' rather than fatal — ignore the error if idempotent deletion is desired.
- Verify the OID via SELECT loid FROM pg_largeobject_metadata WHERE oid=$1 before unlinking if you need to distinguish missing vs. failure.
- Ensure Unlink runs inside the same transaction that owns the large object.
- Re-read the OID fresh (e.g. via lo_create or pg_largeobject_metadata) rather than caching across sessions.
Example fix
// before
if err := lo.Unlink(ctx, oid); err != nil { return err } // fails if already gone
// after — tolerate already-removed
if err := lo.Unlink(ctx, oid); err != nil {
if err.Error() == "failed to remove large object" { /* already gone */ return nil }
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// confirm existence first if you need to distinguish missing vs. failure
var exists int
conn.QueryRow(ctx, "select count(*) from pg_largeobject_metadata where oid=$1", oid).Scan(&exists)
if exists == 0 { return nil } // idempotent unlink Try / catch
if err := los.Unlink(ctx, oid); err != nil {
if err.Error() == "failed to remove large object" {
// already gone — treat as success
return nil
}
return err
} Prevention
- Treat Unlink 'failed to remove' as not-found and tolerate it for idempotent deletes.
- Keep large-object operations inside the owning transaction.
- Re-read OIDs fresh rather than caching across sessions.
When it happens
Trigger: Calling Unlink(ctx, oid) with an OID that does not correspond to an existing large object, or where the server could not delete it (e.g. it was already unlinked, or the OID is invalid).
Common situations: Deleting a large object that was already removed in another transaction; passing a stale OID cached from a prior session; a race where another process unlinked first; calling Unlink outside a transaction (large objects are transactional).
Related errors
- failed to write to large object
- short write to large object
- invalid write to large object
- invalid read of large object
AI-assisted analysis of jackc/pgx@ec1a0befd2 (2026-08-04).
Data as JSON: /data/errors/093474d9805e9d67.json.
Report an issue: GitHub.