microsoft/typescript-go · error

CFStringCreate returned NULL

Error message

CFStringCreate returned NULL

What it means

On macOS, building an FSEvents stream first converts each watched path to a CFString; if CoreFoundation's CFStringCreate returns NULL (allocation failure), this sentinel error is returned and the stream is never created. It is effectively an out-of-memory condition at the CF layer.

Source

Thrown at internal/fswatch/fsevents_darwin.go:213

func (b *fsEventsBackend) start() error {
	b.notifyStarted()
	return nil
}

// checkWatcher mirrors the helper of the same name.
func checkWatcher(w *dirWatch) error {
	info, err := os.Stat(w.physicalDir)
	if err != nil {
		return &dirWatchError{err: err, dirWatch: w}
	}
	if !info.IsDir() {
		return &dirWatchError{err: syscall.ENOTDIR, dirWatch: w}
	}
	return nil
}

var (
	errCFStringCreateNull = errors.New("CFStringCreate returned NULL")
	errCFArrayCreateNull  = errors.New("CFArrayCreate returned NULL")
	errStreamCreateNull   = errors.New("FSEventStreamCreate returned NULL")
	errStreamStartFailed  = errors.New("error starting FSEvents stream")
)

var (
	errFSEventsUserDropped   = fmt.Errorf("events were dropped by the FSEvents client: %w", ErrOverflow)
	errFSEventsKernelDropped = fmt.Errorf("events were dropped by the kernel: %w", ErrOverflow)
	errFSEventsTooMany       = fmt.Errorf("too many events: %w", ErrOverflow)
)

const fseventsPathsPerStream = 512

type fseventsWatchSnapshot struct {
	w     *dirWatch
	state *fseventsState
}

View on GitHub (pinned to 1bcfa18d79)

Solutions

  1. Free memory / raise the container or runner memory limit before creating watches
  2. Reduce the number and length of watched paths (watch fewer roots, prune ignores) to shrink allocations
  3. Retry once after a pause — transient CF allocation failures usually clear when memory is released
  4. Fall back to a polling watcher if FSEvents cannot allocate

Example fix

// before
w, err := fswatch.Default().WatchDirectories(allRequests) // thousands of roots, CFStringCreate NULL

// after: watch few roots recursively instead of listing every directory
w, err := fswatch.Default().WatchDirectory(repoRoot, cb, fswatch.WithRecursive(),
    fswatch.WithIgnore(ignoreHeavyDirs))
Defensive patterns

Strategy: retry

Validate before calling

// Check available memory before building large FSEvents subscriptions.
func memHeadroom() bool {
    si, err := mem.VirtualMemory() // github.com/pod coconut gopsutil/v3/mem
    if err != nil { return true }
    return si.Available > 512*1024*1024 // require >= 512MB free
}

Type guard

func isCFAllocFailure(err error) bool {
    return err != nil && (errors.Is(err, fswatch.ErrUnavailable) == false) &&
        (strings.Contains(err.Error(), "CFStringCreate returned NULL") ||
         strings.Contains(err.Error(), "CFArrayCreate returned NULL"))
}

Try / catch

w, err := watcher.WatchDirectories(reqs)
if err != nil && isCFAllocFailure(err) {
    time.Sleep(2 * time.Second) // let memory pressure clear
    if w, err = watcher.WatchDirectories(reqs); isCFAllocFailure(err) {
        return startPolling(roots(reqs)) // degrade to polling
    }
}
if err != nil { return err }

Prevention

When it happens

Trigger: Creating a watch while the process/system is out of memory; requesting very many paths in one batch so the per-path CFString allocations fail; running inside memory-capped containers/CI executors on macOS runners.

Common situations: Watchers over huge path lists (monorepo recursive watches) on memory-pressured machines; long-running daemons with leaks that finally exhaust the heap; macOS CI hosts near their memory limit.

Related errors


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