JavaRush /Courses /Go SELF /File error classes — not exist, permission, invalid path

File error classes — not exist, permission, invalid path

Go SELF
Level 39, Lesson 4
Available

1. Why split file errors into classes

When you start working with files, the first reaction to any problem usually looks like this: “Oh, it didn’t open — oh well.” But very quickly it turns out that why it didn’t open is more important than the fact itself. The file might not exist (and that’s normal on the first run), there might be permission issues (and that needs to be explained to the user), or the path might be invalid altogether (and that’s a reason to fix input data or configuration).

In Go it’s extremely harmful to “recognize” errors by their text (err.Error()), because the text depends on the OS, locale, and the specific system call. A key idea of the language is that errors are values and often carry structured information, not just a string. So we learn to determine the error class (not exist / permission / invalid path) programmatically, not “by vibes.”

As a reference point, it helps to keep a simple table in mind:

Error class What it means in plain language Typical program action
not exist
“The file/directory doesn’t exist” Create the file/directory or accept an “empty state”
permission
“No permission to read/write” Show a clear message, suggest the right path/permissions
invalid path
“The path is invalid” Fix input/config, not “retry”

2. Class not exist: the file isn’t there — and that’s often OK

The “does not exist” error is the friendliest of file errors. It often means not a disaster, but that the program is being run for the first time, the data isn’t there yet, the directory hasn’t been created, or the user provided an incorrect path. It’s important to learn to distinguish this scenario from “something seriously broke,” because the response should be different.

In Go there’s a convenient check for this: os.IsNotExist(err). It works even if the error is wrapped in multiple layers, as long as you wrapped it using %w. This directly supports the idea “add context, but don’t break cause detection.”

Mini example: “if the file doesn’t exist, treat the task list as empty”

Below is a small snippet for our training app “mini-todo,” where tasks are stored in a file (one task per line). If the file doesn’t exist, return an empty list and a nil error.


package main

import (
	"os"
)

func loadTasks(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil && os.IsNotExist(err) {
		return []byte{}, nil
	}
	return b, err
}

Notice the style: we don’t say “there is no error,” we say “the error is expected and is treated as empty data.”

3. Class permission: “the file exists, but you’re not allowed”

Permission errors are usually more painful, because the program seems to be doing everything right, the file seems to exist, but the OS says: “no.” And that makes sense: the operating system protects data. In practice the reason may be mundane (running without required privileges, the file belongs to another user, the directory is not writable), but to the user it always looks like “your program is broken.”

In Go this class is convenient to check with os.IsPermission(err). The idea is to avoid saying “something went wrong,” and instead give a clear message: “no permission to read/write at path …”. Technical details can be kept in the wrapped error, but outward-facing (especially in a CLI) should be short and to the point.

Mini example: a write attempt and a polite message

package main

import (
	"fmt"
	"os"
)

func saveTasks(path string, data []byte) error {
	err := os.WriteFile(path, data, 0644)
	if err != nil && os.IsPermission(err) {
		return fmt.Errorf("no permission to write file %q: %w", path, err)
	}
	return err
}

Here we do two things at once: detect the permission class and add context (which file, which operation). This “two-layer” approach is normal Go practice: clear context on the outside, recognizable cause on the inside.

4. Class invalid path: not obvious, but important

With invalid path there’s unpleasant news: unlike not exist and permission, it is not always recognized by a single universal function like os.IsInvalidPath. The reason is simple: what exactly counts as an “invalid path” depends on the OS and the file system. Some places forbid certain characters, others forbid different ones; some places the path can be “too long,” others can reject a particular sequence.

But there’s good news too: at the very least, you should be able to understand that the problem is the path itself, and not try to fix it by creating directories, retrying, or other magic. invalid path is usually a bug in input/config, not a transient failure.

A practical “rock-solid” example of an invalid path: \x00

There is one trick that works almost everywhere the same way: a null byte in the path (\x00). It is not allowed for system calls.

package main

import (
	"fmt"
	"os"
)

func main() {
	_, err := os.ReadFile("\x00")
	fmt.Printf("err=%v\n", err) // err=open <...>: invalid argument (wording depends on the OS)
}

The text will differ across systems, and that’s normal. The main point: even if it seems like “a path is just a string,” the OS treats it as a strictly constrained format.

5. Why you shouldn’t compare errors by err.Error()

The temptation is understandable: “well, I see the text permission denied, so I’ll just check strings.Contains.” The problem is that this breaks as soon as you change the environment. On another OS the text is different. On the same OS but for a different operation, it can also be different. Even the format may vary: sometimes the path is included in the message, sometimes it isn’t.

Go nudges you to work with errors as values: wrap them for context and check the cause via errors.Is/errors.As or functions like os.IsNotExist/os.IsPermission. This is one of the basic engineering habits in the Go ecosystem.

To “see” the correct order of actions, it helps to think in terms of a handling protocol like this:

flowchart TD
    A["Perform a file operation (ReadFile/WriteFile/OpenFile)"] --> B{err == nil?}
    B -->|yes| C["Continue"]
    B -->|no| D{"os.IsNotExist(err)?"}
    D -->|yes| E["'First run' scenario: empty data / create directory"]
    D -->|no| F{"os.IsPermission(err)?"}
    F -->|yes| G["Message to user: no permission + path"]
    F -->|no| H["Treat as unexpected: wrap and return upward"]

6. Useful nuances: *os.PathError and wrapping %w

When you print an error from os.ReadFile or os.Open, it often looks like it already includes everything: operation, path, cause. But for the program this matters too: sometimes you don’t just want to output “error,” you want to understand which operation failed (open, read, stat), with which path, and what cause is inside.

For this, Go often uses the structured error *os.PathError. It stores fields Op, Path, Err. And a nice part: even if you wrapped the error with %w, you can still extract *os.PathError via errors.As. This is a direct continuation of “errors as values, not strings.”

Mini example: extracting PathError for diagnostics

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	_, err := os.ReadFile("no_such_file.txt")
	if err == nil {
		return
	}

	var pe *os.PathError
	if errors.As(err, &pe) {
		fmt.Println(pe.Op)   // open
		fmt.Println(pe.Path) // no_such_file.txt
		fmt.Println(pe.Err)  // specific cause (depends on the OS)
	}
}

This isn’t about “pretty output for the user,” it’s about diagnostics: as a developer, you quickly understand what exactly happened.

When you write file-related code, you almost always need to return the error upward with a “human explanation”: what you were doing and with which path. And it’s easy to shoot yourself in the foot: if you wrap the error with %v, cause detection breaks; if you wrap with %w, cause detection is preserved, but details of the underlying error “leak” outward.

In application code (especially in apps), it’s usually useful to preserve the cause so the top level can make a decision. That’s exactly what %w does. This model “context outside, cause inside” is standard for modern Go.

Mini example: “correct” wrapping

package main

import (
	"fmt"
	"os"
)

func load(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("load todo file %q: %w", path, err)
	}
	return b, nil
}

If later you call os.IsNotExist(err) or errors.Is(err, os.ErrNotExist), it will keep working.

7. Example: mini-todo with error classification

Now let’s assemble the ideas into one small scenario. Suppose we have the file data/todo/items.txt. Our program tries to read tasks and then (for simplicity) just prints how many tasks were found. We want:

  • if the file doesn’t exist, treat it as an empty list;
  • if there is no permission, say it in plain language;
  • if the path is invalid, don’t pretend “the file is simply missing.”

Step 1: reading with classification

package main

import (
	"errors"
	"fmt"
	"os"
)

func readTodoFile(path string) ([]byte, error) {
	b, err := os.ReadFile(path)
	if err == nil {
		return b, nil
	}
	if os.IsNotExist(err) {
		return []byte{}, nil
	}
	if os.IsPermission(err) {
		return nil, fmt.Errorf("no permission to read %q: %w", path, err)
	}
	return nil, fmt.Errorf("failed to read %q: %w", path, err)
}

func main() {
	_, err := readTodoFile("data/todo/items.txt")
	if err != nil && errors.Is(err, os.ErrPermission) {
		fmt.Println("permission problem") // example branch (details above)
	}
}

Yes, we “over-guard” a bit in main, but that’s fine for a learning example: it shows that wrapping preserves the cause.

Step 2: human-readable response without “walls of text”

Below is an example function that turns a technical error into a short message. The key is not to swing to extremes: don’t hide the error entirely, but also don’t dump a three-screen stack trace on the user.

package main

import (
	"fmt"
	"os"
)

func formatFileError(path string, err error) string {
	if os.IsNotExist(err) {
		return fmt.Sprintf("file or directory not found: %s", path)
	}
	if os.IsPermission(err) {
		return fmt.Sprintf("no permission to access file: %s", path)
	}
	return fmt.Sprintf("file operation error %s: %v", path, err)
}

Here we intentionally keep the third branch generic. Why? Because invalid path and many other causes can look different, and our goal is to give the user a clear result without pretending we classified everything perfectly.

8. Common mistakes

Mistake #1: checking common file problems via err.Error() and strings.Contains.
This code looks working right up until the first OS change or the first situation where the error text is phrased differently. It’s much more reliable to think in classes: os.IsNotExist, os.IsPermission, and for diagnostics — errors.As(err, &pe) to *os.PathError.

Mistake #2: wrapping an error with %v, and then wondering why os.IsNotExist “stopped working.”
If you need a recognizable cause, use %w. Otherwise you leave only a string from the cause, and a string, as we’ve already learned, is finicky and platform-dependent.

Mistake #3: treating not exist as a “fatal” error in all scenarios.
For many applications, the absence of a data file is a normal first-run state. If you crash immediately with an error, you make the program “fragile”: it can’t start in a clean environment.

Mistake #4: trying to “fix” permission by creating directories and retrying.
Permissions are not a temporary network glitch. If the OS says “no,” usually you need to change the path, run with different privileges, or adjust file-system permissions. Retries here usually just annoy the user.

Mistake #5: confusing “file not found” with “invalid path.”
These are different worlds: “not found” may mean you should create the file; “invalid path” more often means an input/config problem. It helps to remember: invalid path is not a reason to create directories “just in case,” it’s a reason to look at what string is being passed into your file function.

1
Task
Go SELF, level 39, lesson 4
Locked
Storage sensor
Storage sensor
1
Task
Go SELF, level 39, lesson 4
Locked
Saving the pass
Saving the pass
1
Task
Go SELF, level 39, lesson 4
Locked
Smart loader
Smart loader
1
Task
Go SELF, level 39, lesson 4
Locked
Troubleshooting file paths
Troubleshooting file paths
1
Survey/quiz
Files, 39 level, 4 lesson
Unavailable
Files
Files
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION