What is syscall.Errno, and how do you test whether an error is a specific errno such as ENOENT?
answer
- it is just a number
- an integer type with an Error method
- os hands you a path error
- unwrap before you compare
- errors.Is with the named constant
basics
~20 ssyscall.Errno is an integer type (a uintptr) whose Error method makes the number itself satisfy Go's error interface. Test for a particular errno with errors.Is(err, syscall.ENOENT), which unwraps wrappers such as *os.PathError instead of matching message text.
solid answer
~40 s`syscall.Errno` is declared as `type Errno uintptr` — the raw errno number the kernel returned, with an `Error() string` method, so the value itself is an `error`. The constants `syscall.ENOENT`, `syscall.EACCES`, `syscall.EINTR` and friends are values of that type, and because it is a plain integer it is comparable with `==`. You rarely get it bare, though: `os.Open` returns a `*os.PathError` whose `Err` field holds the Errno, so `err == syscall.ENOENT` is false while `errors.Is(err, syscall.ENOENT)` is true — `errors.Is` walks the `Unwrap` chain. Errno also has an `Is` method mapping a few values onto portable sentinels (ENOENT to `os.ErrNotExist`, EACCES and EPERM to `os.ErrPermission`), which is how the same check works on Windows. Never compare `err.Error()` against "no such file or directory".
code
go · 6 lines_, err := os.Open("/etc/does-not-exist")
// err's dynamic type is *os.PathError, not syscall.Errno
fmt.Println(err == syscall.ENOENT) // false
fmt.Println(errors.Is(err, syscall.ENOENT)) // true
fmt.Println(errors.Is(err, os.ErrNotExist)) // truego deeper
Be ready to say that syscall.Errno is an integer type that satisfies error, and to write the errors.Is check against a named constant. Knowing why string matching on the message is wrong is the whole point of the question.
Explain the wrapping chain: os returns *os.PathError holding the Errno, so errors.Is unwraps where == cannot. Mention that Errno has an Is method mapping ENOENT and EACCES onto the portable os sentinels.
Show the production instinct: branch on ENOENT and EACCES during a filesystem walk instead of aborting, and make it testable by constructing a *os.PathError with the errno you want rather than staging a real failure on disk.
Own the convention: decide whether your packages expose raw errnos as part of their contract or translate them into domain errors at the boundary, since every errno a caller matches on becomes something you cannot stop returning.
## What syscall.Errno is A Unix system call reports failure by handing back a small non-zero integer — the *errno*. Go models that integer directly: ```go type Errno uintptr ``` That is the whole type. It has a value-receiver `Error() string` method, so an `Errno` value *is* an `error` with no pointer and no allocation, and it is comparable with `==` like any integer. The named constants — `syscall.ENOENT`, `syscall.EACCES`, `syscall.EPERM`, `syscall.EEXIST`, `syscall.EINTR`, `syscall.EAGAIN` — are typed `Errno` values. Beyond `Error`, the type carries `Is(target error) bool`, `Temporary() bool` and `Timeout() bool`. Use the **named constant**, never the number. The name is portable across operating systems; the numeric value behind it is not, and a hard-coded `2` is a bug waiting for the first non-Linux build. ## Zero means success, and zero is not nil The generated low-level wrappers return the errno as an `Errno`, not as an `error`, precisely because zero means "no error": ```go var err error = syscall.Errno(0) fmt.Println(err == nil) // false ``` An interface value holds a (type, value) pair; assigning a zero `Errno` into an `error` makes the interface non-nil even though the errno says success. That is why hand-written wrappers check `if errno != 0 { return errno }` and return a literal `nil` otherwise. Getting this backwards produces a function that always looks like it failed. ## Where the errno actually reaches you The standard library almost never hands you a bare `Errno`. It wraps: - `os.Open`, `os.Stat`, `os.Remove` return `*os.PathError` — fields `Op`, `Path`, `Err`, where `Err` is the `Errno`. - `os.Rename`, `os.Link` return `*os.LinkError`. - Socket operations return `*net.OpError`, often wrapping `*os.SyscallError`, which wraps the `Errno`. So a direct comparison fails: ```go _, err := os.Open("/etc/does-not-exist") fmt.Println(err == syscall.ENOENT) // false: err is *os.PathError fmt.Println(errors.Is(err, syscall.ENOENT)) // true ``` `errors.Is` unwraps repeatedly, comparing at each level and consulting any `Is` method it finds, which is exactly what a layered error tree needs. A type assertion — `err.(syscall.Errno)` — fails for the same reason the `==` does: the dynamic type is the wrapper, not the errno. ## The portable check `Errno.Is` maps a handful of values onto the operating-system-independent sentinels, so `errors.Is(err, os.ErrNotExist)` matches ENOENT, `os.ErrPermission` matches EACCES and EPERM, and `os.ErrExist` matches EEXIST. Prefer those sentinels when the *category* is what you care about; drop to `syscall.ENOTEMPTY`, `syscall.ENOSPC`, `syscall.EMFILE` or `syscall.EINTR` when you need the specific kernel condition, because no portable sentinel exists for them. ## Why string matching is the wrong instinct Engineers arriving from languages where errors are strings reach for `strings.Contains(err.Error(), "no such file")`. That breaks in three ways: the wording differs between operating systems, it silently matches a path that happens to contain the phrase, and it couples your control flow to a message the runtime is free to reword. `errors.Is` is a value comparison through a documented chain; the string is a human-readable rendering of it. ## In a file-tree indexer A daemon walking directories hits errnos constantly, and the branch is usually three-way: ENOENT because a file was deleted between the directory read and the `stat` (skip it, this is normal), EACCES because a subtree is not readable by this user (log once and prune), and anything else (surface it). Writing that as three `errors.Is` checks against named constants keeps the walk readable and keeps the test honest, because a test can construct `&os.PathError{Err: syscall.EACCES}` and assert the walker's decision without needing a real unreadable directory on the CI machine. ## Checklist - Compare with `errors.Is`, not `==`, unless you produced the error yourself one line earlier. - Use named constants; never the numeric value. - Treat `Errno(0)` as success and return a real `nil`. - Reach for `os.ErrNotExist` / `os.ErrPermission` when the category suffices, the raw errno when it does not.
- Why does errors.Is(err, syscall.ENOENT) succeed on an error from os.Open when a plain == comparison fails?os.Open returns a `*os.PathError` carrying the operation, the path, and an `Err` field holding the `syscall.Errno`. A `==` comparison sees the wrapper's dynamic type and fails. `errors.Is` follows the `Unwrap` chain, comparing at each level, so it reaches the errno inside. The same reason makes a `err.(syscall.Errno)` type assertion fail.
- A wrapper returns syscall.Errno(0) as its error result. Is the caller's err == nil?No. Assigning a zero `Errno` into an `error` interface produces a non-nil interface holding type `Errno` and value 0, so `err == nil` is false and every caller sees a phantom failure. Errno 0 means success, so a wrapper must compare `errno != 0` and return an explicit literal `nil` otherwise.
- When should you check syscall.ENOENT directly rather than os.ErrNotExist?Use `os.ErrNotExist` when you only need the category and want the check to hold on Windows too — `syscall.Errno.Is` maps ENOENT onto it. Drop to the raw errno when no portable sentinel exists for the condition you care about: ENOSPC, EMFILE, ENOTEMPTY, EINTR. Those have no cross-platform stand-in, and the named constant is still portable source even where the number is not.
The errno is the numeric code stamped on a rejection slip; the message under it is only a translation of that code, and translations vary by office.
saying these in an interview costs you the question
- Matches err.Error() against the text no such file or directory
- Uses err == syscall.ENOENT on an error returned by os.Open
- Thinks syscall.Errno is a struct needing a type assertion first
- Assumes a zero Errno assigned to an error becomes nil
- Hard-codes the numeric errno value instead of the named constant
- Believes the errno number is identical on every operating system