Skip to content
gohacks.dev
Basics

No Such File or Directory Errors in Go Programs Fixed

Fix no such file or directory in Go: wrong working directory, missing parent folders, stray newlines in paths, dangling symlinks and missing container files.

6 min read
Summarize in AI
Flat illustration of a log with a failing line and a wrench.

open config.yaml: no such file or directory is the first runtime error many Go programs print. The message is exact and unhelpful, because it never says where the program looked. The file exists on your machine, you are sure of it, and the program disagrees. The answer is almost always that the path is not the path you think it is.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

This page lists the causes in the order they occur, with a reproduction and fix for each. It belongs to the error handling guide, has a close relative in permission denied, and uses the file functions from reading files in Go. Everything ran on Go 1.25.6.

First, ask where it looked

The message includes the path you gave. If it was relative, the process resolved it against its current working directory, so the first diagnostic is to print that directory and the resolved path:

main.goGo
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
)

func main() {
	wd, _ := os.Getwd()
	fmt.Println("working directory ends in:", filepath.Base(wd))

	_, err := os.ReadFile("config.yaml")
	fmt.Println(err)
	fmt.Println("not exist:", errors.Is(err, fs.ErrNotExist))

	abs, _ := filepath.Abs("config.yaml")
	fmt.Println("resolved to a path ending:", filepath.Base(filepath.Dir(abs))+"/config.yaml")
}
Text
working directory ends in: where
open config.yaml: no such file or directory
not exist: true
resolved to a path ending: where/config.yaml

errors.Is(err, fs.ErrNotExist) is the portable test for this error. It matches ENOENT on Unix and the corresponding Windows codes, through any layers of wrapping, so use it instead of string matching or the older os.IsNotExist, which does not see through fmt.Errorf("%w"). The resolved absolute path answers the actual question: is that where the file is? If not, the cause is one of the following.

Cause 1: the working directory is not what you expect

This is the most common one by far. A relative path is relative to where the process was started, not to where the source file or the binary lives.

  • go run . uses the directory you ran it from.
  • go test sets the working directory to the package's directory, so a test that opens testdata/sample.json works there, and the same relative path from main run at the repository root may not.
  • A compiled binary run by a service manager or cron starts in / or the user's home, so files next to the binary are not found.
  • A container starts in the image's WORKDIR, which may not contain your files unless the Dockerfile copied them.

The fixes depend on where the file belongs. For files that ship with the program, embed them with //go:embed, which puts them in the binary and removes the question:

main.goGo
package main

import (
	_ "embed"
	"fmt"
)

//go:embed greeting.txt
var greeting string

func main() {
	fmt.Print(greeting)
}
Text
Hello from an embedded file
greeting.txtText
Hello from an embedded file

The file is read at build time and compiled in, so it cannot be missing at run time. For files that the user or operator supplies, take an absolute path from a flag or an environment variable (environment variables), and print it in the error. For paths relative to the executable, resolve them from os.Executable() rather than assuming the working directory.

Cause 2: the parent directory does not exist

os.Create, os.OpenFile with O_CREATE and os.WriteFile create the file, never its directories. Writing logs/app.log into a missing logs directory fails with the same message, which is confusing because you are creating, not opening:

main.goGo
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
)

func main() {
	dir, _ := os.MkdirTemp("", "demo")
	defer os.RemoveAll(dir)
	os.Chdir(dir)

	err := os.WriteFile("logs/app.log", []byte("x"), 0o644)
	fmt.Println("without MkdirAll:", errors.Is(err, fs.ErrNotExist))

	if err := os.MkdirAll(filepath.Dir("logs/app.log"), 0o755); err != nil {
		panic(err)
	}
	err = os.WriteFile("logs/app.log", []byte("x"), 0o644)
	fmt.Println("with MkdirAll:   ", err)
}
Text
without MkdirAll: true
with MkdirAll:    <nil>

os.MkdirAll creates every missing directory in the path and succeeds if they already exist, so it is safe to call unconditionally before writing. Mind the permission bits (0o755 for directories) and the process umask. If the directory is created by another process, a race between the check and the write can still fail; creating the directory and writing in the same function, as above, is the pattern.

Cause 3: whitespace or a newline in the path

A path read from standard input, a file or an environment variable often carries a trailing newline, and that character becomes part of the file name you are looking for:

main.goGo
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"strings"
)

func main() {
	dir, _ := os.MkdirTemp("", "demo")
	defer os.RemoveAll(dir)
	os.Chdir(dir)
	os.WriteFile("data.txt", []byte("ok"), 0o644)

	typed := "data.txt\n" // what bufio.Reader.ReadString('\n') returns

	_, err := os.ReadFile(typed)
	fmt.Println("raw:    ", errors.Is(err, fs.ErrNotExist))

	_, err = os.ReadFile(strings.TrimSpace(typed))
	fmt.Println("trimmed:", err)
}
Text
raw:     true
trimmed: <nil>

The raw path fails and the trimmed one works. Trim input at the point you read it (reading user input shows bufio.Scanner, which strips the newline for you). When you print a path in an error, use %q so a newline shows up escaped in the message and not as an unexplained line break.

If the path is a symbolic link whose target does not exist, opening it reports no such file or directory even though the link itself is there, and ls shows it:

main.goGo
package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main() {
	dir, _ := os.MkdirTemp("", "demo")
	defer os.RemoveAll(dir)
	os.Chdir(dir)

	os.Symlink("missing-target.txt", "link.txt")

	_, err := os.Open("link.txt")
	fmt.Println("open:  ", errors.Is(err, fs.ErrNotExist))

	info, err := os.Lstat("link.txt")
	fmt.Println("lstat: ", err, info.Mode()&fs.ModeSymlink != 0)
}
Text
open:   true
lstat:  <nil> true

os.Open follows the link and finds nothing at the end. os.Lstat looks at the link itself and succeeds, with the symlink mode bit set. If an error message names a path you can see in a listing, check with Lstat or ls -l for a dangling link; deployment tooling that creates links before the files they point to is a frequent source.

Cause 5: it is not the file that is missing

The same message appears when a program tries to run a binary and something it needs is missing. On Linux, executing a dynamically linked binary whose interpreter or shared library does not exist reports no such file or directory for the executable itself, which is why a Go binary built with cgo and copied into a minimal container (scratch, or Alpine without glibc) fails with that error even though the file is plainly present. The remedy is to build statically with CGO_ENABLED=0 or match the libc in the image (what ships inside every Go binary).

When starting a subprocess, a missing program gives a different error: exec: "tool": executable file not found in $PATH. That one is about the search path, not the file system.

A checklist

  1. Print the full path with %q, and os.Getwd() once.
  2. Is it relative? Run from the directory you think, or switch to an absolute path or an embed.
  3. Creating a file? Call os.MkdirAll on its directory first.
  4. Does the path come from input? Trim it.
  5. Is it a symlink? Check with Lstat.
  6. In a container? List the directory inside it, and check the WORKDIR and the copied files.
  7. A binary that "exists" but won't run? Check its linking with file and ldd.

Detect the condition with errors.Is(err, fs.ErrNotExist), and decide whether it is an error at all: for an optional config file, a missing file is the normal case, and the right code uses defaults instead of failing. The io/fs package documents the portable error values, and the os.MkdirAll reference covers the directory creation.

Taggederror fix
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.