microsoft/typescript-go · error · ErrClientError

%w: empty handle

Error message

%w: empty handle

What it means

release rejects params that are nil or carry Snapshot == 0. Snapshot handles are nonzero IDs minted by the server (snapshotHandle of the project snapshot's ID), so a zero value means the JSON omitted the snapshot field and it decoded to the Go zero value. This is a request-shape bug, not a double-release; releasing an already-released handle produces a different error ("snapshot N not found").

Source

Thrown at internal/api/session.go:1121

		projectResponses = append(projectResponses, NewProjectResponse(proj))
	}

	// Compute changes from the requested base snapshot so the client can retain
	// cached source files for unchanged files.
	changes := computeSnapshotChanges(baseSD.snapshot, snapshot)

	return &UpdateSnapshotResponse{
		Snapshot: handle,
		Projects: projectResponses,
		Changes:  changes,
	}, nil
}

// handleRelease decrements the ref count for a snapshot.
// The snapshot and its registries are only cleaned up when the ref count reaches zero.
func (s *Session) handleRelease(ctx context.Context, params *ReleaseParams) (any, error) {
	if params == nil || params.Snapshot == 0 {
		return nil, fmt.Errorf("%w: empty handle", ErrClientError)
	}

	if err := s.releaseSnapshot(params.Snapshot); err != nil {
		return nil, err
	}
	return true, nil
}

// handleGetDefaultProjectForFile returns the default project for a given file,
// or nil if no project currently contains the file.
func (s *Session) handleGetDefaultProjectForFile(ctx context.Context, params *GetDefaultProjectForFileParams) (*ProjectResponse, error) {
	sd, err := s.getSnapshotData(params.Snapshot)
	if err != nil {
		return nil, err
	}

	uri := params.File.ToURI(s.projectSession.GetCurrentDirectory())
	proj := sd.snapshot.GetDefaultProject(uri)

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Only call release when you hold a real handle from an UpdateSnapshotResponse
  2. Set your local handle variable to zero after a successful release and skip zero values
  3. On error paths, release only handles that were actually acquired

Example fix

// before
if err != nil { release(0) } // no snapshot ever acquired

// after
if err != nil { if snap != 0 { release(snap) }; return err }
Defensive patterns

Strategy: validation

Validate before calling

func releaseIfHeld(snap *api.SnapshotID) error {
    if snap == nil || *snap == 0 { return nil } // nothing acquired
    _, err := session.HandleRequest(ctx, string(api.MethodRelease), api.ReleaseParams{Snapshot: *snap})
    if err == nil { *snap = 0 }
    return err
}

Type guard

func isLiveSnapshot(id api.SnapshotID) bool { return id != 0 }

Try / catch

if _, err := session.HandleRequest(ctx, string(api.MethodRelease), params); err != nil {
    if strings.Contains(err.Error(), "empty handle") {
        // request-shape bug: your snapshot field was omitted; fix the caller, do not retry
    }
    return err
}

Prevention

When it happens

Trigger: Sending {} or null to release; omitting the snapshot key in the payload; a client variable left uninitialized when no prior updateSnapshot ran.

Common situations: Cleanup paths that release unconditionally on error, before any snapshot was ever obtained; refcount tracking that loses the handle when earlier calls failed.

Related errors


AI-assisted analysis of microsoft/typescript-go@1bcfa18d79 (2026-08-16). Data as JSON: /api/errors/258b8eb08cf718f0. Report an issue: GitHub.