LEARN · DEBUGGING GUIDE

Debugging go.mod replace Directive Errors

replace directives are powerful but brittle. A single typo, path mismatch, or version inconsistency can break your entire build. Here's how to fix them fast.

IntermediateGo6 min read

What this usually means

The replace directive instructs the Go toolchain to use an alternate module source instead of the one specified in the require block. Errors usually come from three categories: (1) the syntax is wrong (version mismatch, missing or extra slashes), (2) the target path doesn't exist or is not a valid Go module (no go.mod), or (3) there's a conflict between multiple replace directives (e.g., in a workspace or transitive dependency). The go tool's error messages are often terse: 'missing module', 'invalid version', or 'conflicting replacement'. These always point to a mismatch between what go.mod says and what the filesystem or module proxy can provide.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1go mod why -m <module> — see if the module is actually required
  • 2go mod edit -replace <old>=<new> — test a corrected replace in-place
  • 3go mod verify — check that dependencies haven't been tampered with
  • 4go list -m all | grep <module> — confirm the resolved version
  • 5go clean -modcache && go mod download — rebuild the module cache from scratch
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchgo.mod in the root of your project
  • searchgo.sum — check for hash mismatches after replace
  • searchAny go.work file if using Go workspaces
  • searchThe target directory's go.mod (e.g., ../local-repo/go.mod)
  • searchgo env GOMODCACHE — cache location; inspect for stale copies
  • searchCI logs: often the relative path works locally but CI runs from a different working directory
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningTypo in module path or version string (case-sensitive on Linux, version must be semver)
  • warningRelative path in replace directive that doesn't exist or lacks a go.mod
  • warningVersion mismatch: replace uses a version that doesn't exist in the target
  • warningTransitive dependency conflict: a dependency also has a replace for the same module
  • warningGo workspace (go.work) overrides project-level replace directives
  • warningStale module cache: go mod download cached a previous broken version
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildFor invalid version: check the target module's actual tags with `git tag`; use exact version like v1.2.3
  • buildFor missing module path: ensure the path is correct and the target has go.mod; `go mod init` if needed
  • buildFor conflicting replacements: remove duplicate replace directives or use go.work to centralize
  • buildFor CI path issues: use absolute paths or environment variables (e.g., `replace example.com/m => /ci/workspace/m`)
  • buildFor cache staleness: `go clean -modcache` then rebuild
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedgo build ./... — clean build from scratch after fix
  • verifiedgo test ./... — ensure tests pass
  • verifiedgo list -m -json <module> — verify resolved version matches the replace target
  • verifiedgo mod verify — check checksums match
  • verifiedIn CI: add `pwd` and `ls -la` steps to print working directory and check file existence
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningUsing replace for a module that is not actually required (go mod tidy may remove it)
  • warningAssuming replace works with any path — the target must be a directory with a valid go.mod
  • warningForgetting to run `go mod tidy` after adding a replace directive
  • warningMixing replace and exclude for the same module without understanding precedence
  • warningCommitting a replace directive that works only on your machine (e.g., absolute paths)
  • warningOverwriting go.sum manually; always let `go mod tidy` regenerate it
( 07 )War story

The CI Build That Worked Locally But Failed Everywhere Else

Platform Engineer at a mid-stage startupGo 1.20, microservices monorepo with shared libraries, CI on GitHub Actions

Timeline

  1. 09:15Pushed a branch that updated a shared library's API
  2. 09:17CI fails with 'missing module: example.com/shared v0.4.0' even though I added a replace directive in go.mod
  3. 09:20Checked CI logs: replace directive points to ../shared (relative path)
  4. 09:25Locally `go build` works fine; I run `go list -m all | grep shared` and see version 0.4.0 replaced
  5. 09:30Realized CI checks out only the service repo, not the sibling shared repo
  6. 09:35Updated CI workflow to also checkout shared repo with a fixed path
  7. 09:40Push fix; CI still fails — now error says 'invalid version: unknown revision v0.4.0' for shared
  8. 09:45Look at shared repo's git tags: there is no v0.4.0 tag; we use branch-based development
  9. 09:50Changed replace to use a branch: `replace example.com/shared => ../shared v0.0.0-20230301-branch`
  10. 09:55CI passes. Root cause: relative path not available in CI and version tag didn't exist.

The morning started with a simple API change in our shared library. I updated a function signature, bumped the version in go.mod to v0.4.0, and pushed. Our microservice depended on that library, so I added a replace directive to test locally: `replace example.com/shared => ../shared`. Local builds were green. I felt confident.

CI failed immediately. The error was 'missing module: example.com/shared v0.4.0'. I dove into the logs. The replace directive was there, but the path '../shared' didn't exist in CI. Our pipeline only checked out the service repo, not the sibling. I updated the workflow to check out both repos, setting a fixed path. Push again.

Now a different error: 'invalid version: unknown revision v0.4.0'. Confused, I listed git tags in the shared repo — there was no v0.4.0. We used a branch-based workflow and never tagged that version. The replace directive expected a version that didn't exist. I changed it to use a pseudo-version from the branch: `replace example.com/shared => ../shared v0.0.0-20230301-branch`. CI passed. The lesson: replace requires an actual version or commit hash, not just a path.

Root cause

The replace directive used a version tag that didn't exist in the target module, and the relative path wasn't available in CI.

The fix

Updated replace to use a valid pseudo-version and ensured CI checked out both repos with consistent paths.

The lesson

Always validate that the version in the replace directive actually exists in the target module. For local development, use a branch or commit hash. For CI, ensure the target path is accessible.

( 08 )How replace Directives Are Resolved

When go toolchain encounters a replace directive, it substitutes the module path in the require block with the specified target. The target can be a module path with a version (e.g., `example.com/foo v1.2.3`), a local directory (e.g., `../local-fork`), or a version from a different module proxy. The resolution order is: workspace go.work > project go.mod > indirect dependencies. If a dependency's go.mod also has a replace for the same module, you get a 'conflicting replacement' error unless the workspace unifies them.

A common mistake is assuming replace with a local path automatically uses the latest commit. It does not — it uses the version specified after the path (if any) or the version from the go.mod of the target directory. If no version is given, go uses the version from the target's go.mod. If that version is not a valid semver like v0.0.0-<timestamp>-<hash>, go may error. Always check the target's go.mod for its own 'require' block or version.

( 09 )Workspace Overrides and Conflicting Replacements

Go workspaces (go.work) can define their own replace directives that take precedence over individual module go.mod files. If you're in a workspace, run `go work edit -replace` to add a workspace-level replace. The error 'conflicting replacements' means at least two modules in your workspace define a replace for the same module path with different targets. To fix, either remove the duplicate replaces or unify them in go.work.

Example: module A's go.mod has `replace example.com/foo => ./foo`, module B's go.mod has `replace example.com/foo => ./bar`. When building with a workspace that includes both, you get an error. Solution: remove the replaces from A and B and add a single replace in go.work: `replace example.com/foo => ./foo`.

( 10 )The go.sum Mismatch Trap

When you add a replace directive, go mod tidy may update go.sum to include checksums for the replaced module. If the replace target changes (e.g., you rebase a local branch), the checksums in go.sum become stale. You'll see 'verification failure' or 'checksum mismatch'. The fix: run `go mod tidy` to regenerate go.sum. If that doesn't work, `go clean -modcache` and try again.

Never edit go.sum manually. Always let the go tool manage it. If you're in a monorepo with shared go.sum, ensure all developers run `go mod tidy` after pulling changes that include replace directives. Otherwise, they'll get hash mismatches.

( 11 )CI/CD Pitfalls with Relative Paths

Relative paths in replace directives are relative to the module's directory. In CI, the working directory may differ from your local machine. Common patterns: use environment variables like `$GITHUB_WORKSPACE` to construct absolute paths, or use a script to set the replace path dynamically before build. Another approach: commit a separate go.mod for CI that uses version tags instead of local paths.

If your CI checks out multiple repos, ensure they are placed at the same relative locations as in your local setup. A robust method: use `go mod edit -replace` in a CI step to inject the correct absolute path. Example: `go mod edit -replace example.com/foo=$CI_PROJECT_DIR/../foo`.

Frequently asked questions

Why does `go mod tidy` remove my replace directive?

go mod tidy removes replace directives for modules that are not actually required by your code. If you have a replace for a module that is not in your require block or is not transitively imported, tidy deletes it. Add a require statement or ensure the module is imported somewhere.

Can I use a replace directive with a Git branch or commit hash?

Yes. Use a pseudo-version like `v0.0.0-20230301123456-abcdef123456` or a branch name with the `go mod` tool. However, replace with a local directory does not automatically use the latest commit; you must specify the version explicitly or ensure the target's go.mod has a valid version.

What does 'conflicting replacement' mean and how do I fix it?

It means two or more modules in your build (including workspace modules) define a replace for the same module path with different targets. To fix, centralize the replace in the workspace go.work file, or remove the duplicates and keep only one.

Why does replace work locally but not in CI?

Most common reasons: the relative path doesn't exist in CI (CI may not check out sibling repos), the target module's version tag doesn't exist, or the working directory is different. Use absolute paths or environment variables in CI, and ensure the target's go.mod has a valid version.

Can I use replace with a module from a different module proxy?

Yes. For example, `replace example.com/foo => myproxy.com/foo v1.2.3`. This tells go to fetch the module from a different proxy. Ensure the proxy is accessible and the version exists.