Field Notes


6 min read

From Air to Watchexec

A Developer's Journey to Better File Watching

As developers, we’re creatures of habit. We find tools that work, and we stick with them—sometimes long past their expiration date. For years, I was that developer who swore by Golang’s Air for hot reloading during development. It was simple, it worked (mostly), and it kept my Go projects humming along nicely. But like many long-term relationships, what started as convenience eventually became compromise.

The breaking point came when I started working more heavily with multi-module projects and shared libraries. Suddenly, Air’s limitations weren’t just minor inconveniences—they were productivity killers.

The Air We Used to Breathe

Don’t get me wrong—Air served its purpose well for single-directory Go projects. The setup was straightforward: drop in a configuration file, run air, and watch your code changes trigger automatic rebuilds and restarts. For the typical web service or CLI application contained in a single module, it was perfect.

But modern development isn’t always that simple. Today’s projects often span multiple directories, include shared libraries, and require watching various file types across different locations. This is where Air started showing its age.

The most frustrating limitation was Air’s struggle with multiple directory watching. When you’re developing a library that’s consumed by multiple services, or working with a monorepo structure, you need your file watcher to be aware of changes across various paths. Air’s approach to this scenario felt clunky and unreliable. Sometimes changes wouldn’t trigger rebuilds, other times false positives would cause unnecessary restarts.

I found myself spending more time debugging my development setup than actually developing.

Enter Watchexec: The Rust-Powered Alternative

The transition to watchexec wasn’t immediate—and I’ll admit, it stung a little. Here I was, a Go enthusiast, abandoning a Go tool for something written in Rust. It felt like betrayal, but sometimes you have to swallow your pride for the sake of productivity.

Watchexec is a generic command runner that executes arbitrary commands when files change. This might sound simple, but that simplicity is exactly what makes it powerful.

What Makes Watchexec Special

The first thing you notice about watchexec is its flexibility. Rather than being opinionated about what you’re building or how you want to build it, it simply asks: “What command should I run when files change?” This generic approach means you can use it for Go projects, Node.js applications, Python scripts, or anything else that benefits from automatic recompilation or restart.

But the real magic happens when you start exploring its configuration options. Watchexec handles multiple directory watching elegantly. You can specify multiple paths, use glob patterns, and even exclude specific files or directories with intuitive syntax. What took complex configuration gymnastics in Air becomes straightforward command-line arguments in watchexec.

The performance difference is immediately noticeable. Watchexec’s file system event handling feels snappier and more reliable. Changes are detected quickly and accurately, without the occasional missed updates that plagued my Air setup. The Rust foundation shows—the tool feels solid and responsive even when watching large directory trees.

One feature that particularly impressed me is watchexec’s ability to handle different file types intelligently. You can configure different commands for different file extensions, or set up complex filtering rules that would have been nightmare to implement with Air. Need to restart your Go service when .go files change, but only rebuild your frontend when .js or .css files are modified? Watchexec handles this naturally.

Getting Started with Watchexec

Installing watchexec is straightforward across different platforms. If you’re on macOS with Homebrew:

brew install watchexec

For Linux systems, you can use your distribution’s package manager, or grab a pre-built binary from the GitHub releases page. Windows users can install it via Scoop or Chocolatey:

# Using Scoop
scoop install watchexec

# Using Chocolatey
choco install watchexec

Alternatively, if you have Rust installed, you can install it directly from crates.io:

cargo install watchexec-cli

Practical Examples for Go Development

Let’s look at some real-world usage patterns that showcase watchexec’s strengths for Go projects.

For a basic Go web application, replace your Air command with:

watchexec -r -e go -- go run ./cmd/server

The -r flag ensures the previous process is killed before starting a new one, and -e go limits watching to Go files only.

For a more complex setup where you want to watch multiple directories (perhaps you have shared libraries or configuration files):

watchexec -w src/ -w internal/ -w config/ -e go,yaml,json --restart -- go run ./cmd/server

This watches the src/, internal/, and config/ directories for changes to Go, YAML, and JSON files.

If you’re working with a Go module that includes generated code, you might want to exclude certain directories:

watchexec -w . -i '*_test.go' -i '*/vendor/*' -i '*/node_modules/*' -e go --restart -- go run ./cmd/server

The -i flag ignores specific patterns—in this case, test files and vendor directories.

For projects that require building before running, you can chain commands:

watchexec -e go --restart -- sh -c 'go build ./cmd/server && ./server'

Or if you prefer to see build output more clearly:

watchexec -e go --restart -- sh -c 'echo "Building..." && go build -o ./tmp/server ./cmd/server && echo "Starting server..." && ./tmp/server'

The Migration Experience

Migrating from Air to watchexec was surprisingly smooth. Instead of replacing my .air.toml configuration file with another complex config, I found myself using simple shell commands that were easier to understand and modify.

The learning curve was minimal. The command-line interface is intuitive, and the documentation is excellent. Within a day, I had migrated all my development workflows and was already seeing the benefits.

Beyond the Technical: A Mindset Shift

What struck me most about this transition wasn’t just the technical improvements—it was the mindset shift. Watchexec’s generic approach made me think differently about development tooling. Instead of looking for language-specific solutions, I started considering more flexible, composable tools that could adapt to different workflows.

This philosophy extends beyond just file watching. The best development tools are often those that do one thing exceptionally well and integrate seamlessly with other tools, rather than trying to be comprehensive solutions for specific languages or frameworks.

Is It Worth the Switch?

If you’re currently using Air and it’s working perfectly for your needs, there’s no urgent reason to change. But if you’ve encountered any of the limitations I mentioned—particularly around multi-directory watching or complex project structures—watchexec is absolutely worth evaluating.

The installation is straightforward (it’s available through most package managers), the migration is low-risk (you can easily switch back if needed), and the potential productivity gains are significant. Even if you’re not experiencing problems with your current setup, the improved performance and flexibility might surprise you.

Final Thoughts

Tools evolve, and so should our willingness to evolve with them. Sometimes the best solutions come from unexpected places—in this case, a Rust-based generic file watcher proving superior to a Go-specific tool for Go development.

The transition from Air to watchexec taught me that language loyalty, while admirable, shouldn’t override practical benefits. The best tool for the job isn’t always the one written in your preferred language, and that’s perfectly fine.

If you’re on the fence about making a similar switch, my advice is simple: give watchexec a try on a small project. The investment in learning a new tool is minimal, but the payoff in improved development experience might be significant. Sometimes the most productive thing you can do is step outside your comfort zone—even if it means letting a little Rust into your Go-centric world.

More in Field Notes

Share