microsoft/typescript-go · error · dirWatchError

invalid handle: %w

Error message

invalid handle: %w

What it means

Synchronous error from WatchDirectory on Windows: CreateFile failed to open the watched directory with FILE_LIST_DIRECTORY access and FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OVERLAPPED. The %w wraps the Win32 error (a windows.Errno), so errors.Is works against specific codes. Note the separate returns next to it: a successful open whose GetFileInformationByHandle fails yields errGetFileInfo, and a non-directory yields syscall.ENOTDIR.

Source

Thrown at internal/fswatch/windows.go:178

	event      windows.Handle
}

func newWindowsSubscription(watcherImpl *windowsBackend, w *dirWatch) (*windowsSubscription, error) {
	pathPtr, err := windows.UTF16PtrFromString(w.physicalDir)
	if err != nil {
		return nil, &dirWatchError{err: err, dirWatch: w}
	}
	h, err := windows.CreateFile(
		pathPtr,
		windows.FILE_LIST_DIRECTORY,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED,
		0,
	)
	if err != nil {
		return nil, &dirWatchError{err: fmt.Errorf("invalid handle: %w", err), dirWatch: w}
	}
	var info windows.ByHandleFileInformation
	if err := windows.GetFileInformationByHandle(h, &info); err != nil {
		_ = windows.CloseHandle(h)
		return nil, &dirWatchError{err: errGetFileInfo, dirWatch: w}
	}
	if info.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
		_ = windows.CloseHandle(h)
		return nil, &dirWatchError{err: syscall.ENOTDIR, dirWatch: w}
	}
	return &windowsSubscription{
		watcherImpl: watcherImpl,
		dirWatch:    w,

		handle:   h,
		stopCh:   make(chan struct{}),
		doneCh:   make(chan struct{}),
		bufBytes: defaultBufSize,

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Verify with os.Stat(dir) that the path exists and is a directory immediately before subscribing
  2. Fix ACLs or run under an account with list-directory rights on the target
  3. Retry in a loop when the directory is created asynchronously at startup
  4. Branch on the wrapped errno: errors.Is(err, windows.ERROR_FILE_NOT_FOUND) suggests a race, ERROR_ACCESS_DENIED suggests permissions

Example fix

// before
watch, err := fswatch.Windows().WatchDirectory(dir, cb)
if err != nil {
    return err // no retry: races with dir creation fail permanently
}

// after
var watch fswatch.Watch
for attempt := 0; attempt < 10; attempt++ {
    var err error
    watch, err = fswatch.Windows().WatchDirectory(dir, cb)
    if err == nil {
        break
    }
    if !errors.Is(err, windows.ERROR_FILE_NOT_FOUND) && !errors.Is(err, windows.ERROR_PATH_NOT_FOUND) {
        return err
    }
    time.Sleep(100 * time.Millisecond)
}
Defensive patterns

Strategy: try-catch

Validate before calling

info, err := os.Stat(dir)
if err != nil {
    return err
}
if !info.IsDir() {
    return syscall.ENOTDIR
}

Try / catch

watch, err := fswatch.Windows().WatchDirectory(dir, cb)
if err != nil {
    switch {
    case errors.Is(err, windows.ERROR_FILE_NOT_FOUND), errors.Is(err, windows.ERROR_PATH_NOT_FOUND):
        // race with deletion/creation: retry
    case errors.Is(err, windows.ERROR_ACCESS_DENIED):
        // fix ACLs or account
    default:
        return err
    }
}

Prevention

When it happens

Trigger: Directory does not exist (ERROR_FILE_NOT_FOUND or ERROR_PATH_NOT_FOUND), for example deleted between your existence check and the subscribe call. Access denied (ERROR_ACCESS_DENIED) because the process lacks list-directory rights. Sharing or path problems on unusual reparse points.

Common situations: Watching a directory a build step just removed. Unprivileged service accounts watching protected paths. Misconfigured paths, including typos and wrong case on case-sensitive volumes.

Related errors


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