skip to content

What is syscall.Errno, and how do you test whether an error is a specific errno such as ENOENT?

level: juniorimportance: must knowfreq 38%

answer

  1. it is just a number
  2. an integer type with an Error method
  3. os hands you a path error
  4. unwrap before you compare
  5. errors.Is with the named constant

basics

~20 s

syscall.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
go
_, 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))  // true

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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