Repository: prometheus/procfs
Pull request #508 (2023-04-23 08:29:49)

Add net/route parse, also add util lib Pars…
first time to contribute to this repo, might 
adds trivial parse of file net/route.
@pgier 

sansna: passed tests
sansna: any advices, welcome
discordianfish: Look at the linter errors, still need toget fixed.
sansna: > Look at the linter errors, still need toget fixed.

definitely, recently i got a job, don't have much time, sorry and thanks.
sansna: please check again, thanks for your help

Patch:
diff --git a/arp.go b/arp.go
index 68f36e8..28783e2 100644
--- a/arp.go
+++ b/arp.go
@@ -55,7 +55,7 @@ type ARPEntry struct {
 func (fs FS) GatherARPEntries() ([]ARPEntry, error) {
 	data, err := os.ReadFile(fs.proc.Path("net/arp"))
 	if err != nil {
-		return nil, fmt.Errorf("error reading arp %q: %w", fs.proc.Path("net/arp"), err)
+		return nil, fmt.Errorf("%s: error reading arp %s: %w", ErrFileRead, fs.proc.Path("net/arp"), err)
 	}
 
 	return parseARPEntries(data)
@@ -78,11 +78,11 @@ func parseARPEntries(data []byte) ([]ARPEntry, error) {
 		} else if width == expectedDataWidth {
 			entry, err := parseARPEntry(columns)
 			if err != nil {
-				return []ARPEntry{}, fmt.Errorf("failed to parse ARP entry: %w", err)
+				return []ARPEntry{}, fmt.Errorf("%s: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err)
 			}
 			entries = append(entries, entry)
 		} else {
-			return []ARPEntry{}, fmt.Errorf("%d columns were detected, but %d were expected", width, expectedDataWidth)
+			return []ARPEntry{}, fmt.Errorf("%s: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err)
 		}
 
 	}
diff --git a/buddyinfo.go b/buddyinfo.go
index f5b7939..4a17363 100644
--- a/buddyinfo.go
+++ b/buddyinfo.go
@@ -55,7 +55,7 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
 		parts := strings.Fields(line)
 
 		if len(parts) < 4 {
-			return nil, fmt.Errorf("invalid number of fields when parsing buddyinfo")
+			return nil, fmt.Errorf("%w: Invalid number of fields, found: %v", ErrFileParse, parts)
 		}
 
 		node := strings.TrimRight(parts[1], ",")
@@ -66,7 +66,7 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
 			bucketCount = arraySize
 		} else {
 			if bucketCount != arraySize {
-				return nil, fmt.Errorf("mismatch in number of buddyinfo buckets, previous count %d, new count %d", bucketCount, arraySize)
+				return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize)
 			}
 		}
 
@@ -74,7 +74,7 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
 		for i := 0; i < arraySize; i++ {
 			sizes[i], err = strconv.ParseFloat(parts[i+4], 64)
 			if err != nil {
-				return nil, fmt.Errorf("invalid value in buddyinfo: %w", err)
+				return nil, fmt.Errorf("%s: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err)
 			}
 		}
 
diff --git a/cpuinfo.go b/cpuinfo.go
index 06968ca..f4f5501 100644
--- a/cpuinfo.go
+++ b/cpuinfo.go
@@ -79,7 +79,7 @@ func parseCPUInfoX86(info []byte) ([]CPUInfo, error) {
 	// find the first "processor" line
 	firstLine := firstNonEmptyLine(scanner)
 	if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%w: Cannot parse  line: %q", ErrFileParse, firstLine)
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	v, err := strconv.ParseUint(field[1], 0, 32)
@@ -192,9 +192,10 @@ func parseCPUInfoARM(info []byte) ([]CPUInfo, error) {
 	scanner := bufio.NewScanner(bytes.NewReader(info))
 
 	firstLine := firstNonEmptyLine(scanner)
-	match, _ := regexp.MatchString("^[Pp]rocessor", firstLine)
+	match, err := regexp.MatchString("^[Pp]rocessor", firstLine)
 	if !match || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%s: Cannot parse line: %q: %w", ErrFileParse, firstLine, err)
+
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	cpuinfo := []CPUInfo{}
@@ -258,7 +259,7 @@ func parseCPUInfoS390X(info []byte) ([]CPUInfo, error) {
 
 	firstLine := firstNonEmptyLine(scanner)
 	if !strings.HasPrefix(firstLine, "vendor_id") || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%w: Cannot parse line: %q", ErrFileParse, firstLine)
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	cpuinfo := []CPUInfo{}
@@ -283,7 +284,7 @@ func parseCPUInfoS390X(info []byte) ([]CPUInfo, error) {
 		if strings.HasPrefix(line, "processor") {
 			match := cpuinfoS390XProcessorRegexp.FindStringSubmatch(line)
 			if len(match) < 2 {
-				return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+				return nil, fmt.Errorf("%w: %q", ErrFileParse, firstLine)
 			}
 			cpu := commonCPUInfo
 			v, err := strconv.ParseUint(match[1], 0, 32)
@@ -343,7 +344,7 @@ func parseCPUInfoMips(info []byte) ([]CPUInfo, error) {
 	// find the first "processor" line
 	firstLine := firstNonEmptyLine(scanner)
 	if !strings.HasPrefix(firstLine, "system type") || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%w: %q", ErrFileParse, firstLine)
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	cpuinfo := []CPUInfo{}
@@ -421,7 +422,7 @@ func parseCPUInfoPPC(info []byte) ([]CPUInfo, error) {
 
 	firstLine := firstNonEmptyLine(scanner)
 	if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%w: %q", ErrFileParse, firstLine)
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	v, err := strconv.ParseUint(field[1], 0, 32)
@@ -466,7 +467,7 @@ func parseCPUInfoRISCV(info []byte) ([]CPUInfo, error) {
 
 	firstLine := firstNonEmptyLine(scanner)
 	if !strings.HasPrefix(firstLine, "processor") || !strings.Contains(firstLine, ":") {
-		return nil, fmt.Errorf("invalid cpuinfo file: %q", firstLine)
+		return nil, fmt.Errorf("%w: %q", ErrFileParse, firstLine)
 	}
 	field := strings.SplitN(firstLine, ": ", 2)
 	v, err := strconv.ParseUint(field[1], 0, 32)
diff --git a/crypto.go b/crypto.go
index 5048ad1..9a73e26 100644
--- a/crypto.go
+++ b/crypto.go
@@ -55,12 +55,13 @@ func (fs FS) Crypto() ([]Crypto, error) {
 	path := fs.proc.Path("crypto")
 	b, err := util.ReadFileNoStat(path)
 	if err != nil {
-		return nil, fmt.Errorf("error reading crypto %q: %w", path, err)
+		return nil, fmt.Errorf("%s: Cannot read file %v: %w", ErrFileRead, b, err)
+
 	}
 
 	crypto, err := parseCrypto(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("error parsing crypto %q: %w", path, err)
+		return nil, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, crypto, err)
 	}
 
 	return crypto, nil
@@ -83,7 +84,7 @@ func parseCrypto(r io.Reader) ([]Crypto, error) {
 
 		kv := strings.Split(text, ":")
 		if len(kv) != 2 {
-			return nil, fmt.Errorf("malformed crypto line: %q", text)
+			return nil, fmt.Errorf("%w: Cannot parae line: %q", ErrFileParse, text)
 		}
 
 		k := strings.TrimSpace(kv[0])
diff --git a/fs_statfs_notype.go b/fs_statfs_notype.go
index 446a85d..8005769 100644
--- a/fs_statfs_notype.go
+++ b/fs_statfs_notype.go
@@ -11,8 +11,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//go:build netbsd || openbsd || solaris
-// +build netbsd openbsd solaris
+//go:build netbsd || openbsd || solaris || windows
+// +build netbsd openbsd solaris windows
 
 package procfs
 
diff --git a/fs_statfs_type.go b/fs_statfs_type.go
index 6f5af28..6233217 100644
--- a/fs_statfs_type.go
+++ b/fs_statfs_type.go
@@ -11,8 +11,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//go:build !netbsd && !openbsd && !solaris
-// +build !netbsd,!openbsd,!solaris
+//go:build !netbsd && !openbsd && !solaris && !windows
+// +build !netbsd,!openbsd,!solaris,!windows
 
 package procfs
 
diff --git a/fscache.go b/fscache.go
index f8070e6..f560a8d 100644
--- a/fscache.go
+++ b/fscache.go
@@ -236,7 +236,7 @@ func (fs FS) Fscacheinfo() (Fscacheinfo, error) {
 
 	m, err := parseFscacheinfo(bytes.NewReader(b))
 	if err != nil {
-		return Fscacheinfo{}, fmt.Errorf("failed to parse Fscacheinfo: %w", err)
+		return Fscacheinfo{}, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, m, err)
 	}
 
 	return *m, nil
@@ -245,7 +245,7 @@ func (fs FS) Fscacheinfo() (Fscacheinfo, error) {
 func setFSCacheFields(fields []string, setFields ...*uint64) error {
 	var err error
 	if len(fields) < len(setFields) {
-		return fmt.Errorf("Insufficient number of fields, expected %v, got %v", len(setFields), len(fields))
+		return fmt.Errorf("%s: Expected %d, but got %d: %w", ErrFileParse, len(setFields), len(fields), err)
 	}
 
 	for i := range setFields {
@@ -263,7 +263,7 @@ func parseFscacheinfo(r io.Reader) (*Fscacheinfo, error) {
 	for s.Scan() {
 		fields := strings.Fields(s.Text())
 		if len(fields) < 2 {
-			return nil, fmt.Errorf("malformed Fscacheinfo line: %q", s.Text())
+			return nil, fmt.Errorf("%w: malformed Fscacheinfo line: %q", ErrFileParse, s.Text())
 		}
 
 		switch fields[0] {
diff --git a/ipvs.go b/ipvs.go
index 391c079..5a145bb 100644
--- a/ipvs.go
+++ b/ipvs.go
@@ -221,15 +221,16 @@ func parseIPPort(s string) (net.IP, uint16, error) {
 	case 46:
 		ip = net.ParseIP(s[1:40])
 		if ip == nil {
-			return nil, 0, fmt.Errorf("invalid IPv6 address: %s", s[1:40])
+			return nil, 0, fmt.Errorf("%s: Invalid IPv6 addr %s: %w", ErrFileParse, s[1:40], err)
 		}
 	default:
-		return nil, 0, fmt.Errorf("unexpected IP:Port: %s", s)
+		return nil, 0, fmt.Errorf("%s: Unexpected IP:Port %s: %w", ErrFileParse, s, err)
 	}
 
 	portString := s[len(s)-4:]
 	if len(portString) != 4 {
-		return nil, 0, fmt.Errorf("unexpected port string format: %s", portString)
+		return nil, 0,
+			fmt.Errorf("%s: Unexpected port string format %s: %w", ErrFileParse, portString, err)
 	}
 	port, err := strconv.ParseUint(portString, 16, 16)
 	if err != nil {
diff --git a/loadavg.go b/loadavg.go
index 0096caf..59465c5 100644
--- a/loadavg.go
+++ b/loadavg.go
@@ -44,14 +44,14 @@ func parseLoad(loadavgBytes []byte) (*LoadAvg, error) {
 	loads := make([]float64, 3)
 	parts := strings.Fields(string(loadavgBytes))
 	if len(parts) < 3 {
-		return nil, fmt.Errorf("malformed loadavg line: too few fields in loadavg string: %q", string(loadavgBytes))
+		return nil, fmt.Errorf("%w: Malformed line %q", ErrFileParse, string(loadavgBytes))
 	}
 
 	var err error
 	for i, load := range parts[0:3] {
 		loads[i], err = strconv.ParseFloat(load, 64)
 		if err != nil {
-			return nil, fmt.Errorf("could not parse load %q: %w", load, err)
+			return nil, fmt.Errorf("%s: Cannot parse load: %f: %w", ErrFileParse, loads[i], err)
 		}
 	}
 	return &LoadAvg{
diff --git a/mdstat.go b/mdstat.go
index a95c889..fdd4b95 100644
--- a/mdstat.go
+++ b/mdstat.go
@@ -70,7 +70,7 @@ func (fs FS) MDStat() ([]MDStat, error) {
 	}
 	mdstat, err := parseMDStat(data)
 	if err != nil {
-		return nil, fmt.Errorf("error parsing mdstat %q: %w", fs.proc.Path("mdstat"), err)
+		return nil, fmt.Errorf("%s: Cannot parse %v: %w", ErrFileParse, fs.proc.Path("mdstat"), err)
 	}
 	return mdstat, nil
 }
@@ -90,13 +90,13 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 
 		deviceFields := strings.Fields(line)
 		if len(deviceFields) < 3 {
-			return nil, fmt.Errorf("not enough fields in mdline (expected at least 3): %s", line)
+			return nil, fmt.Errorf("%s: Expected 3+ lines, got %q", ErrFileParse, line)
 		}
 		mdName := deviceFields[0] // mdx
 		state := deviceFields[2]  // active or inactive
 
 		if len(lines) <= i+3 {
-			return nil, fmt.Errorf("error parsing %q: too few lines for md device", mdName)
+			return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName)
 		}
 
 		// Failed disks have the suffix (F) & Spare disks have the suffix (S).
@@ -105,7 +105,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 		active, total, down, size, err := evalStatusLine(lines[i], lines[i+1])
 
 		if err != nil {
-			return nil, fmt.Errorf("error parsing md device lines: %w", err)
+			return nil, fmt.Errorf("%s: Cannot parse md device lines: %v: %w", ErrFileParse, active, err)
 		}
 
 		syncLineIdx := i + 2
@@ -140,7 +140,7 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 			} else {
 				syncedBlocks, pct, finish, speed, err = evalRecoveryLine(lines[syncLineIdx])
 				if err != nil {
-					return nil, fmt.Errorf("error parsing sync line in md device %q: %w", mdName, err)
+					return nil, fmt.Errorf("%s: Cannot parse sync line in md device: %q: %w", ErrFileParse, mdName, err)
 				}
 			}
 		}
@@ -168,13 +168,13 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
 func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
 	statusFields := strings.Fields(statusLine)
 	if len(statusFields) < 1 {
-		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q", statusLine)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
 	}
 
 	sizeStr := statusFields[0]
 	size, err = strconv.ParseInt(sizeStr, 10, 64)
 	if err != nil {
-		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
 	}
 
 	if strings.Contains(deviceLine, "raid0") || strings.Contains(deviceLine, "linear") {
@@ -189,17 +189,17 @@ func evalStatusLine(deviceLine, statusLine string) (active, total, down, size in
 
 	matches := statusLineRE.FindStringSubmatch(statusLine)
 	if len(matches) != 5 {
-		return 0, 0, 0, 0, fmt.Errorf("couldn't find all the substring matches: %s", statusLine)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Could not fild all substring matches %s: %w", ErrFileParse, statusLine, err)
 	}
 
 	total, err = strconv.ParseInt(matches[2], 10, 64)
 	if err != nil {
-		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
 	}
 
 	active, err = strconv.ParseInt(matches[3], 10, 64)
 	if err != nil {
-		return 0, 0, 0, 0, fmt.Errorf("unexpected statusLine %q: %w", statusLine, err)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected active %d: %w", ErrFileParse, active, err)
 	}
 	down = int64(strings.Count(matches[4], "_"))
 
@@ -209,42 +209,42 @@ func evalStatusLine(deviceLine, statusLine string) (active, total, down, size in
 func evalRecoveryLine(recoveryLine string) (syncedBlocks int64, pct float64, finish float64, speed float64, err error) {
 	matches := recoveryLineBlocksRE.FindStringSubmatch(recoveryLine)
 	if len(matches) != 2 {
-		return 0, 0, 0, 0, fmt.Errorf("unexpected recoveryLine: %s", recoveryLine)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected recoveryLine %s: %w", ErrFileParse, recoveryLine, err)
 	}
 
 	syncedBlocks, err = strconv.ParseInt(matches[1], 10, 64)
 	if err != nil {
-		return 0, 0, 0, 0, fmt.Errorf("error parsing int from recoveryLine %q: %w", recoveryLine, err)
+		return 0, 0, 0, 0, fmt.Errorf("%s: Unexpected parsing of recoveryLine %q: %w", ErrFileParse, recoveryLine, err)
 	}
 
 	// Get percentage complete
 	matches = recoveryLinePctRE.FindStringSubmatch(recoveryLine)
 	if len(matches) != 2 {
-		return syncedBlocks, 0, 0, 0, fmt.Errorf("unexpected recoveryLine matching percentage: %s", recoveryLine)
+		return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching percentage %s", ErrFileParse, recoveryLine)
 	}
 	pct, err = strconv.ParseFloat(strings.TrimSpace(matches[1]), 64)
 	if err != nil {
-		return syncedBlocks, 0, 0, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
+		return syncedBlocks, 0, 0, 0, fmt.Errorf("%w: Error parsing float from recoveryLine %q", ErrFileParse, recoveryLine)
 	}
 
 	// Get time expected left to complete
 	matches = recoveryLineFinishRE.FindStringSubmatch(recoveryLine)
 	if len(matches) != 2 {
-		return syncedBlocks, pct, 0, 0, fmt.Errorf("unexpected recoveryLine matching est. finish time: %s", recoveryLine)
+		return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unexpected recoveryLine matching est. finish time: %s", ErrFileParse, recoveryLine)
 	}
 	finish, err = strconv.ParseFloat(matches[1], 64)
 	if err != nil {
-		return syncedBlocks, pct, 0, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
+		return syncedBlocks, pct, 0, 0, fmt.Errorf("%w: Unable to parse float from recoveryLine: %q", ErrFileParse, recoveryLine)
 	}
 
 	// Get recovery speed
 	matches = recoveryLineSpeedRE.FindStringSubmatch(recoveryLine)
 	if len(matches) != 2 {
-		return syncedBlocks, pct, finish, 0, fmt.Errorf("unexpected recoveryLine matching speed: %s", recoveryLine)
+		return syncedBlocks, pct, finish, 0, fmt.Errorf("%w: Unexpected recoveryLine value: %s", ErrFileParse, recoveryLine)
 	}
 	speed, err = strconv.ParseFloat(matches[1], 64)
 	if err != nil {
-		return syncedBlocks, pct, finish, 0, fmt.Errorf("error parsing float from recoveryLine %q: %w", recoveryLine, err)
+		return syncedBlocks, pct, finish, 0, fmt.Errorf("%s: Error parsing float from recoveryLine: %q: %w", ErrFileParse, recoveryLine, err)
 	}
 
 	return syncedBlocks, pct, finish, speed, nil
diff --git a/meminfo.go b/meminfo.go
index f65e174..eaf00e2 100644
--- a/meminfo.go
+++ b/meminfo.go
@@ -152,7 +152,7 @@ func (fs FS) Meminfo() (Meminfo, error) {
 
 	m, err := parseMemInfo(bytes.NewReader(b))
 	if err != nil {
-		return Meminfo{}, fmt.Errorf("failed to parse meminfo: %w", err)
+		return Meminfo{}, fmt.Errorf("%s: %w", ErrFileParse, err)
 	}
 
 	return *m, nil
@@ -165,7 +165,7 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
 		// Each line has at least a name and value; we ignore the unit.
 		fields := strings.Fields(s.Text())
 		if len(fields) < 2 {
-			return nil, fmt.Errorf("malformed meminfo line: %q", s.Text())
+			return nil, fmt.Errorf("%w: Malformed line %q", ErrFileParse, s.Text())
 		}
 
 		v, err := strconv.ParseUint(fields[1], 0, 64)
diff --git a/mountinfo.go b/mountinfo.go
index 59f4d50..388ebf3 100644
--- a/mountinfo.go
+++ b/mountinfo.go
@@ -78,11 +78,11 @@ func parseMountInfoString(mountString string) (*MountInfo, error) {
 	mountInfo := strings.Split(mountString, " ")
 	mountInfoLength := len(mountInfo)
 	if mountInfoLength < 10 {
-		return nil, fmt.Errorf("couldn't find enough fields in mount string: %s", mountString)
+		return nil, fmt.Errorf("%w: Too few fields in mount string: %s", ErrFileParse, mountString)
 	}
 
 	if mountInfo[mountInfoLength-4] != "-" {
-		return nil, fmt.Errorf("couldn't find separator in expected field: %s", mountInfo[mountInfoLength-4])
+		return nil, fmt.Errorf("%w: couldn't find separator in expected field: %s", ErrFileParse, mountInfo[mountInfoLength-4])
 	}
 
 	mount := &MountInfo{
@@ -98,18 +98,18 @@ func parseMountInfoString(mountString string) (*MountInfo, error) {
 
 	mount.MountID, err = strconv.Atoi(mountInfo[0])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse mount ID")
+		return nil, fmt.Errorf("%w: mount ID: %q", ErrFileParse, mount.MountID)
 	}
 	mount.ParentID, err = strconv.Atoi(mountInfo[1])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse parent ID")
+		return nil, fmt.Errorf("%w: parent ID: %q", ErrFileParse, mount.ParentID)
 	}
 	// Has optional fields, which is a space separated list of values.
 	// Example: shared:2 master:7
 	if mountInfo[6] != "" {
 		mount.OptionalFields, err = mountOptionsParseOptionalFields(mountInfo[6 : mountInfoLength-4])
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("%s: %w", ErrFileParse, err)
 		}
 	}
 	return mount, nil
diff --git a/mountstats.go b/mountstats.go
index 7f68890..852c8c4 100644
--- a/mountstats.go
+++ b/mountstats.go
@@ -266,7 +266,7 @@ func parseMountStats(r io.Reader) ([]*Mount, error) {
 		if len(ss) > deviceEntryLen {
 			// Only NFSv3 and v4 are supported for parsing statistics
 			if m.Type != nfs3Type && m.Type != nfs4Type {
-				return nil, fmt.Errorf("cannot parse MountStats for fstype %q", m.Type)
+				return nil, fmt.Errorf("%w: Cannot parse MountStats for %q", ErrFileParse, m.Type)
 			}
 
 			statVersion := strings.TrimPrefix(ss[8], statVersionPrefix)
@@ -290,7 +290,7 @@ func parseMountStats(r io.Reader) ([]*Mount, error) {
 //	device [device] mounted on [mount] with fstype [type]
 func parseMount(ss []string) (*Mount, error) {
 	if len(ss) < deviceEntryLen {
-		return nil, fmt.Errorf("invalid device entry: %v", ss)
+		return nil, fmt.Errorf("%w: Invalid device %q", ErrFileParse, ss)
 	}
 
 	// Check for specific words appearing at specific indices to ensure
@@ -308,7 +308,7 @@ func parseMount(ss []string) (*Mount, error) {
 
 	for _, f := range format {
 		if ss[f.i] != f.s {
-			return nil, fmt.Errorf("invalid device entry: %v", ss)
+			return nil, fmt.Errorf("%w: Invalid device %q", ErrFileParse, ss)
 		}
 	}
 
@@ -345,7 +345,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 		switch ss[0] {
 		case fieldOpts:
 			if len(ss) < 2 {
-				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+				return nil, fmt.Errorf("%w: Incomplete information for NFS stats: %v", ErrFileParse, ss)
 			}
 			if stats.Opts == nil {
 				stats.Opts = map[string]string{}
@@ -360,7 +360,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 			}
 		case fieldAge:
 			if len(ss) < 2 {
-				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+				return nil, fmt.Errorf("%w: Incomplete information for NFS stats: %v", ErrFileParse, ss)
 			}
 			// Age integer is in seconds
 			d, err := time.ParseDuration(ss[1] + "s")
@@ -371,7 +371,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 			stats.Age = d
 		case fieldBytes:
 			if len(ss) < 2 {
-				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+				return nil, fmt.Errorf("%w: Incomplete information for NFS stats: %v", ErrFileParse, ss)
 			}
 			bstats, err := parseNFSBytesStats(ss[1:])
 			if err != nil {
@@ -381,7 +381,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 			stats.Bytes = *bstats
 		case fieldEvents:
 			if len(ss) < 2 {
-				return nil, fmt.Errorf("not enough information for NFS stats: %v", ss)
+				return nil, fmt.Errorf("%w: Incomplete information for NFS events: %v", ErrFileParse, ss)
 			}
 			estats, err := parseNFSEventsStats(ss[1:])
 			if err != nil {
@@ -391,7 +391,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 			stats.Events = *estats
 		case fieldTransport:
 			if len(ss) < 3 {
-				return nil, fmt.Errorf("not enough information for NFS transport stats: %v", ss)
+				return nil, fmt.Errorf("%w: Incomplete information for NFS transport stats: %v", ErrFileParse, ss)
 			}
 
 			tstats, err := parseNFSTransportStats(ss[1:], statVersion)
@@ -430,7 +430,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
 // integer fields.
 func parseNFSBytesStats(ss []string) (*NFSBytesStats, error) {
 	if len(ss) != fieldBytesLen {
-		return nil, fmt.Errorf("invalid NFS bytes stats: %v", ss)
+		return nil, fmt.Errorf("%w: Invalid NFS bytes stats: %v", ErrFileParse, ss)
 	}
 
 	ns := make([]uint64, 0, fieldBytesLen)
@@ -459,7 +459,7 @@ func parseNFSBytesStats(ss []string) (*NFSBytesStats, error) {
 // integer fields.
 func parseNFSEventsStats(ss []string) (*NFSEventsStats, error) {
 	if len(ss) != fieldEventsLen {
-		return nil, fmt.Errorf("invalid NFS events stats: %v", ss)
+		return nil, fmt.Errorf("%w: invalid NFS events stats: %v", ErrFileParse, ss)
 	}
 
 	ns := make([]uint64, 0, fieldEventsLen)
@@ -523,7 +523,7 @@ func parseNFSOperationStats(s *bufio.Scanner) ([]NFSOperationStats, error) {
 		}
 
 		if len(ss) < minFields {
-			return nil, fmt.Errorf("invalid NFS per-operations stats: %v", ss)
+			return nil, fmt.Errorf("%w: invalid NFS per-operations stats: %v", ErrFileParse, ss)
 		}
 
 		// Skip string operation name for integers
@@ -576,10 +576,10 @@ func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats
 		} else if protocol == "udp" {
 			expectedLength = fieldTransport10UDPLen
 		} else {
-			return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.0 statement: %v", protocol, ss)
+			return nil, fmt.Errorf("%w: Invalid NFS protocol \"%s\" in stats 1.0 statement: %v", ErrFileParse, protocol, ss)
 		}
 		if len(ss) != expectedLength {
-			return nil, fmt.Errorf("invalid NFS transport stats 1.0 statement: %v", ss)
+			return nil, fmt.Errorf("%w: Invalid NFS transport stats 1.0 statement: %v", ErrFileParse, ss)
 		}
 	case statVersion11:
 		var expectedLength int
@@ -588,13 +588,13 @@ func parseNFSTransportStats(ss []string, statVersion string) (*NFSTransportStats
 		} else if protocol == "udp" {
 			expectedLength = fieldTransport11UDPLen
 		} else {
-			return nil, fmt.Errorf("invalid NFS protocol \"%s\" in stats 1.1 statement: %v", protocol, ss)
+			return nil, fmt.Errorf("%w: invalid NFS protocol \"%s\" in stats 1.1 statement: %v", ErrFileParse, protocol, ss)
 		}
 		if len(ss) != expectedLength {
-			return nil, fmt.Errorf("invalid NFS transport stats 1.1 statement: %v", ss)
+			return nil, fmt.Errorf("%w: invalid NFS transport stats 1.1 statement: %v", ErrFileParse, ss)
 		}
 	default:
-		return nil, fmt.Errorf("unrecognized NFS transport stats version: %q", statVersion)
+		return nil, fmt.Errorf("%s: Unrecognized NFS transport stats version: %q", ErrFileParse, statVersion)
 	}
 
 	// Allocate enough for v1.1 stats since zero value for v1.1 stats will be okay
diff --git a/net_conntrackstat.go b/net_conntrackstat.go
index 64a0e94..fdfa456 100644
--- a/net_conntrackstat.go
+++ b/net_conntrackstat.go
@@ -58,7 +58,7 @@ func readConntrackStat(path string) ([]ConntrackStatEntry, error) {
 
 	stat, err := parseConntrackStat(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("failed to read conntrack stats from %q: %w", path, err)
+		return nil, fmt.Errorf("%s: Cannot read file: %v: %w", ErrFileRead, path, err)
 	}
 
 	return stat, nil
@@ -86,11 +86,12 @@ func parseConntrackStat(r io.Reader) ([]ConntrackStatEntry, error) {
 func parseConntrackStatEntry(fields []string) (*ConntrackStatEntry, error) {
 	entries, err := util.ParseHexUint64s(fields)
 	if err != nil {
-		return nil, fmt.Errorf("invalid conntrackstat entry, couldn't parse fields: %s", err)
+		return nil, fmt.Errorf("%s: Cannot parse entry: %d: %w", ErrFileParse, entries, err)
 	}
 	numEntries := len(entries)
 	if numEntries < 16 || numEntries > 17 {
-		return nil, fmt.Errorf("invalid conntrackstat entry, invalid number of fields: %d", numEntries)
+		return nil,
+			fmt.Errorf("%w: invalid conntrackstat entry, invalid number of fields: %d", ErrFileParse, numEntries)
 	}
 
 	stats := &ConntrackStatEntry{
diff --git a/net_ip_socket.go b/net_ip_socket.go
index 7fd57d7..4da81ea 100644
--- a/net_ip_socket.go
+++ b/net_ip_socket.go
@@ -130,7 +130,7 @@ func parseIP(hexIP string) (net.IP, error) {
 	var byteIP []byte
 	byteIP, err := hex.DecodeString(hexIP)
 	if err != nil {
-		return nil, fmt.Errorf("cannot parse address field in socket line %q", hexIP)
+		return nil, fmt.Errorf("%s: Cannot parse socket field in %q: %w", ErrFileParse, hexIP, err)
 	}
 	switch len(byteIP) {
 	case 4:
@@ -144,7 +144,7 @@ func parseIP(hexIP string) (net.IP, error) {
 		}
 		return i, nil
 	default:
-		return nil, fmt.Errorf("Unable to parse IP %s", hexIP)
+		return nil, fmt.Errorf("%s: Unable to parse IP %s: %w", ErrFileParse, hexIP, nil)
 	}
 }
 
@@ -153,7 +153,8 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
 	line := &netIPSocketLine{}
 	if len(fields) < 10 {
 		return nil, fmt.Errorf(
-			"cannot parse net socket line as it has less then 10 columns %q",
+			"%w: Less than 10 columns found %q",
+			ErrFileParse,
 			strings.Join(fields, " "),
 		)
 	}
@@ -162,64 +163,65 @@ func parseNetIPSocketLine(fields []string) (*netIPSocketLine, error) {
 	// sl
 	s := strings.Split(fields[0], ":")
 	if len(s) != 2 {
-		return nil, fmt.Errorf("cannot parse sl field in socket line %q", fields[0])
+		return nil, fmt.Errorf("%w: Unable to parse sl field in line %q", ErrFileParse, fields[0])
 	}
 
 	if line.Sl, err = strconv.ParseUint(s[0], 0, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse sl value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Unable to parse sl field in %q: %w", ErrFileParse, line.Sl, err)
 	}
 	// local_address
 	l := strings.Split(fields[1], ":")
 	if len(l) != 2 {
-		return nil, fmt.Errorf("cannot parse local_address field in socket line %q", fields[1])
+		return nil, fmt.Errorf("%w: Unable to parse local_address field in %q", ErrFileParse, fields[1])
 	}
 	if line.LocalAddr, err = parseIP(l[0]); err != nil {
 		return nil, err
 	}
 	if line.LocalPort, err = strconv.ParseUint(l[1], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse local_address port value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Unable to parse local_address port value line %q: %w", ErrFileParse, line.LocalPort, err)
 	}
 
 	// remote_address
 	r := strings.Split(fields[2], ":")
 	if len(r) != 2 {
-		return nil, fmt.Errorf("cannot parse rem_address field in socket line %q", fields[1])
+		return nil, fmt.Errorf("%w: Unable to parse rem_address field in %q", ErrFileParse, fields[1])
 	}
 	if line.RemAddr, err = parseIP(r[0]); err != nil {
 		return nil, err
 	}
 	if line.RemPort, err = strconv.ParseUint(r[1], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse rem_address port value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse rem_address port value in %q: %w", ErrFileParse, line.RemPort, err)
 	}
 
 	// st
 	if line.St, err = strconv.ParseUint(fields[3], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse st value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse st value in %q: %w", ErrFileParse, line.St, err)
 	}
 
 	// tx_queue and rx_queue
 	q := strings.Split(fields[4], ":")
 	if len(q) != 2 {
 		return nil, fmt.Errorf(
-			"cannot parse tx/rx queues in socket line as it has a missing colon %q",
+			"%w: Missing colon for tx/rx queues in socket line %q",
+			ErrFileParse,
 			fields[4],
 		)
 	}
 	if line.TxQueue, err = strconv.ParseUint(q[0], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse tx_queue value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse tx_queue value in %q: %w", ErrFileParse, line.TxQueue, err)
 	}
 	if line.RxQueue, err = strconv.ParseUint(q[1], 16, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse rx_queue value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse trx_queue value in %q: %w", ErrFileParse, line.RxQueue, err)
 	}
 
 	// uid
 	if line.UID, err = strconv.ParseUint(fields[7], 0, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse uid value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse UID value in %q: %w", ErrFileParse, line.UID, err)
 	}
 
 	// inode
 	if line.Inode, err = strconv.ParseUint(fields[9], 0, 64); err != nil {
-		return nil, fmt.Errorf("cannot parse inode value in socket line: %w", err)
+		return nil, fmt.Errorf("%s: Cannot parse inode value in %q: %w", ErrFileParse, line.Inode, err)
 	}
 
 	return line, nil
diff --git a/net_protocols.go b/net_protocols.go
index 374b6f7..b6c77b7 100644
--- a/net_protocols.go
+++ b/net_protocols.go
@@ -131,7 +131,7 @@ func (ps NetProtocolStats) parseLine(rawLine string) (*NetProtocolStatLine, erro
 	} else if fields[6] == disabled {
 		line.Slab = false
 	} else {
-		return nil, fmt.Errorf("unable to parse capability for protocol: %s", line.Name)
+		return nil, fmt.Errorf("%w: capability for protocol: %s", ErrFileParse, line.Name)
 	}
 	line.ModuleName = fields[7]
 
@@ -173,7 +173,7 @@ func (pc *NetProtocolCapabilities) parseCapabilities(capabilities []string) erro
 		} else if capabilities[i] == "n" {
 			*capabilityFields[i] = false
 		} else {
-			return fmt.Errorf("unable to parse capability block for protocol: position %d", i)
+			return fmt.Errorf("%w: capability block for protocol: position %d", ErrFileParse, i)
 		}
 	}
 	return nil
diff --git a/net_route.go b/net_route.go
new file mode 100644
index 0000000..deb7029
--- /dev/null
+++ b/net_route.go
@@ -0,0 +1,143 @@
+// Copyright 2023 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+	"strconv"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/util"
+)
+
+const (
+	blackholeRepresentation string = "*"
+	blackholeIfaceName      string = "blackhole"
+	routeLineColumns        int    = 11
+)
+
+// A NetRouteLine represents one line from net/route.
+type NetRouteLine struct {
+	Iface       string
+	Destination uint32
+	Gateway     uint32
+	Flags       uint32
+	RefCnt      uint32
+	Use         uint32
+	Metric      uint32
+	Mask        uint32
+	MTU         uint32
+	Window      uint32
+	IRTT        uint32
+}
+
+func (fs FS) NetRoute() ([]NetRouteLine, error) {
+	return readNetRoute(fs.proc.Path("net", "route"))
+}
+
+func readNetRoute(path string) ([]NetRouteLine, error) {
+	b, err := util.ReadFileNoStat(path)
+	if err != nil {
+		return nil, err
+	}
+
+	routelines, err := parseNetRoute(bytes.NewReader(b))
+	if err != nil {
+		return nil, fmt.Errorf("failed to read net route from %s: %w", path, err)
+	}
+	return routelines, nil
+}
+
+func parseNetRoute(r io.Reader) ([]NetRouteLine, error) {
+	var routelines []NetRouteLine
+
+	scanner := bufio.NewScanner(r)
+	scanner.Scan()
+	for scanner.Scan() {
+		fields := strings.Fields(scanner.Text())
+		routeline, err := parseNetRouteLine(fields)
+		if err != nil {
+			return nil, err
+		}
+		routelines = append(routelines, *routeline)
+	}
+	return routelines, nil
+}
+
+func parseNetRouteLine(fields []string) (*NetRouteLine, error) {
+	if len(fields) != routeLineColumns {
+		return nil, fmt.Errorf("invalid routeline, num of digits: %d", len(fields))
+	}
+	iface := fields[0]
+	if iface == blackholeRepresentation {
+		iface = blackholeIfaceName
+	}
+	destination, err := strconv.ParseUint(fields[1], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	gateway, err := strconv.ParseUint(fields[2], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	flags, err := strconv.ParseUint(fields[3], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	refcnt, err := strconv.ParseUint(fields[4], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	use, err := strconv.ParseUint(fields[5], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	metric, err := strconv.ParseUint(fields[6], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	mask, err := strconv.ParseUint(fields[7], 16, 32)
+	if err != nil {
+		return nil, err
+	}
+	mtu, err := strconv.ParseUint(fields[8], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	window, err := strconv.ParseUint(fields[9], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	irtt, err := strconv.ParseUint(fields[10], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+	routeline := &NetRouteLine{
+		Iface:       iface,
+		Destination: uint32(destination),
+		Gateway:     uint32(gateway),
+		Flags:       uint32(flags),
+		RefCnt:      uint32(refcnt),
+		Use:         uint32(use),
+		Metric:      uint32(metric),
+		Mask:        uint32(mask),
+		MTU:         uint32(mtu),
+		Window:      uint32(window),
+		IRTT:        uint32(irtt),
+	}
+	return routeline, nil
+}
diff --git a/net_sockstat.go b/net_sockstat.go
index e36f487..360e36a 100644
--- a/net_sockstat.go
+++ b/net_sockstat.go
@@ -16,7 +16,6 @@ package procfs
 import (
 	"bufio"
 	"bytes"
-	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -70,7 +69,7 @@ func readSockstat(name string) (*NetSockstat, error) {
 
 	stat, err := parseSockstat(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("failed to read sockstats from %q: %w", name, err)
+		return nil, fmt.Errorf("%s: sockstats from %q: %w", ErrFileRead, name, err)
 	}
 
 	return stat, nil
@@ -84,13 +83,13 @@ func parseSockstat(r io.Reader) (*NetSockstat, error) {
 		// Expect a minimum of a protocol and one key/value pair.
 		fields := strings.Split(s.Text(), " ")
 		if len(fields) < 3 {
-			return nil, fmt.Errorf("malformed sockstat line: %q", s.Text())
+			return nil, fmt.Errorf("%w: Malformed sockstat line: %q", ErrFileParse, s.Text())
 		}
 
 		// The remaining fields are key/value pairs.
 		kvs, err := parseSockstatKVs(fields[1:])
 		if err != nil {
-			return nil, fmt.Errorf("error parsing sockstat key/value pairs from %q: %w", s.Text(), err)
+			return nil, fmt.Errorf("%s: sockstat key/value pairs from %q: %w", ErrFileParse, s.Text(), err)
 		}
 
 		// The first field is the protocol. We must trim its colon suffix.
@@ -119,7 +118,7 @@ func parseSockstat(r io.Reader) (*NetSockstat, error) {
 // parseSockstatKVs parses a string slice into a map of key/value pairs.
 func parseSockstatKVs(kvs []string) (map[string]int, error) {
 	if len(kvs)%2 != 0 {
-		return nil, errors.New("odd number of fields in key/value pairs")
+		return nil, fmt.Errorf("%w:: Odd number of fields in key/value pairs %q", ErrFileParse, kvs)
 	}
 
 	// Iterate two values at a time to gather key/value pairs.
diff --git a/net_softnet.go b/net_softnet.go
index 540cea5..c770852 100644
--- a/net_softnet.go
+++ b/net_softnet.go
@@ -64,7 +64,7 @@ func (fs FS) NetSoftnetStat() ([]SoftnetStat, error) {
 
 	entries, err := parseSoftnet(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse /proc/net/softnet_stat: %w", err)
+		return nil, fmt.Errorf("%s: /proc/net/softnet_stat: %w", ErrFileParse, err)
 	}
 
 	return entries, nil
@@ -83,7 +83,7 @@ func parseSoftnet(r io.Reader) ([]SoftnetStat, error) {
 		softnetStat := SoftnetStat{}
 
 		if width < minColumns {
-			return nil, fmt.Errorf("%d columns were detected, but at least %d were expected", width, minColumns)
+			return nil, fmt.Errorf("%w: detected %d columns, but expected at least %d", ErrFileParse, width, minColumns)
 		}
 
 		// Linux 2.6.23 https://elixir.bootlin.com/linux/v2.6.23/source/net/core/dev.c#L2347
diff --git a/net_unix.go b/net_unix.go
index 98aa8e1..acbbc57 100644
--- a/net_unix.go
+++ b/net_unix.go
@@ -108,14 +108,14 @@ func parseNetUNIX(r io.Reader) (*NetUNIX, error) {
 		line := s.Text()
 		item, err := nu.parseLine(line, hasInode, minFields)
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse /proc/net/unix data %q: %w", line, err)
+			return nil, fmt.Errorf("%s: /proc/net/unix encountered data %q: %w", ErrFileParse, line, err)
 		}
 
 		nu.Rows = append(nu.Rows, item)
 	}
 
 	if err := s.Err(); err != nil {
-		return nil, fmt.Errorf("failed to scan /proc/net/unix data: %w", err)
+		return nil, fmt.Errorf("%s: /proc/net/unix encountered data: %w", ErrFileParse, err)
 	}
 
 	return &nu, nil
@@ -126,7 +126,7 @@ func (u *NetUNIX) parseLine(line string, hasInode bool, min int) (*NetUNIXLine,
 
 	l := len(fields)
 	if l < min {
-		return nil, fmt.Errorf("expected at least %d fields but got %d", min, l)
+		return nil, fmt.Errorf("%w: expected at least %d fields but got %d", ErrFileParse, min, l)
 	}
 
 	// Field offsets are as follows:
@@ -136,29 +136,29 @@ func (u *NetUNIX) parseLine(line string, hasInode bool, min int) (*NetUNIXLine,
 
 	users, err := u.parseUsers(fields[1])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse ref count %q: %w", fields[1], err)
+		return nil, fmt.Errorf("%s: ref count %q: %w", ErrFileParse, fields[1], err)
 	}
 
 	flags, err := u.parseFlags(fields[3])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse flags %q: %w", fields[3], err)
+		return nil, fmt.Errorf("%s: Unable to parse flags %q: %w", ErrFileParse, fields[3], err)
 	}
 
 	typ, err := u.parseType(fields[4])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse type %q: %w", fields[4], err)
+		return nil, fmt.Errorf("%s: Failed to parse type %q: %w", ErrFileParse, fields[4], err)
 	}
 
 	state, err := u.parseState(fields[5])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse state %q: %w", fields[5], err)
+		return nil, fmt.Errorf("%s: Failed to parse state %q: %w", ErrFileParse, fields[5], err)
 	}
 
 	var inode uint64
 	if hasInode {
 		inode, err = u.parseInode(fields[6])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse inode %q: %w", fields[6], err)
+			return nil, fmt.Errorf("%s failed to parse inode %q: %w", ErrFileParse, fields[6], err)
 		}
 	}
 
diff --git a/net_wireless.go b/net_wireless.go
index c80fb15..7443edc 100644
--- a/net_wireless.go
+++ b/net_wireless.go
@@ -68,7 +68,7 @@ func (fs FS) Wireless() ([]*Wireless, error) {
 
 	m, err := parseWireless(bytes.NewReader(b))
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse wireless: %w", err)
+		return nil, fmt.Errorf("%s: wireless: %w", ErrFileParse, err)
 	}
 
 	return m, nil
@@ -97,64 +97,64 @@ func parseWireless(r io.Reader) ([]*Wireless, error) {
 
 		parts := strings.Split(line, ":")
 		if len(parts) != 2 {
-			return nil, fmt.Errorf("expected 2 parts after splitting line by ':', got %d for line %q", len(parts), line)
+			return nil, fmt.Errorf("%w: expected 2 parts after splitting line by ':', got %d for line %q", ErrFileParse, len(parts), line)
 		}
 
 		name := strings.TrimSpace(parts[0])
 		stats := strings.Fields(parts[1])
 
 		if len(stats) < 10 {
-			return nil, fmt.Errorf("invalid number of fields in line %d, expected at least 10, got %d: %q", n, len(stats), line)
+			return nil, fmt.Errorf("%w: invalid number of fields in line %d, expected 10+, got %d: %q", ErrFileParse, n, len(stats), line)
 		}
 
 		status, err := strconv.ParseUint(stats[0], 16, 16)
 		if err != nil {
-			return nil, fmt.Errorf("invalid status in line %d: %q", n, line)
+			return nil, fmt.Errorf("%w: invalid status in line %d: %q", ErrFileParse, n, line)
 		}
 
 		qlink, err := strconv.Atoi(strings.TrimSuffix(stats[1], "."))
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:link as integer %q: %w", qlink, err)
+			return nil, fmt.Errorf("%s: parse Quality:link as integer %q: %w", ErrFileParse, qlink, err)
 		}
 
 		qlevel, err := strconv.Atoi(strings.TrimSuffix(stats[2], "."))
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:level as integer %q: %w", qlevel, err)
+			return nil, fmt.Errorf("%s: Quality:level as integer %q: %w", ErrFileParse, qlevel, err)
 		}
 
 		qnoise, err := strconv.Atoi(strings.TrimSuffix(stats[3], "."))
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Quality:noise as integer %q: %w", qnoise, err)
+			return nil, fmt.Errorf("%s: Quality:noise as integer %q: %w", ErrFileParse, qnoise, err)
 		}
 
 		dnwid, err := strconv.Atoi(stats[4])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:nwid as integer %q: %w", dnwid, err)
+			return nil, fmt.Errorf("%s: Discarded:nwid as integer %q: %w", ErrFileParse, dnwid, err)
 		}
 
 		dcrypt, err := strconv.Atoi(stats[5])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:crypt as integer %q: %w", dcrypt, err)
+			return nil, fmt.Errorf("%s: Discarded:crypt as integer %q: %w", ErrFileParse, dcrypt, err)
 		}
 
 		dfrag, err := strconv.Atoi(stats[6])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:frag as integer %q: %w", dfrag, err)
+			return nil, fmt.Errorf("%s: Discarded:frag as integer %q: %w", ErrFileParse, dfrag, err)
 		}
 
 		dretry, err := strconv.Atoi(stats[7])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:retry as integer %q: %w", dretry, err)
+			return nil, fmt.Errorf("%s: Discarded:retry as integer %q: %w", ErrFileParse, dretry, err)
 		}
 
 		dmisc, err := strconv.Atoi(stats[8])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Discarded:misc as integer %q: %w", dmisc, err)
+			return nil, fmt.Errorf("%s: Discarded:misc as integer %q: %w", ErrFileParse, dmisc, err)
 		}
 
 		mbeacon, err := strconv.Atoi(stats[9])
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse Missed:beacon as integer %q: %w", mbeacon, err)
+			return nil, fmt.Errorf("%s: Missed:beacon as integer %q: %w", ErrFileParse, mbeacon, err)
 		}
 
 		w := &Wireless{
@@ -175,7 +175,7 @@ func parseWireless(r io.Reader) ([]*Wireless, error) {
 	}
 
 	if err := scanner.Err(); err != nil {
-		return nil, fmt.Errorf("failed to scan /proc/net/wireless: %w", err)
+		return nil, fmt.Errorf("%s: Failed to scan /proc/net/wireless: %w", ErrFileRead, err)
 	}
 
 	return interfaces, nil
diff --git a/net_xfrm.go b/net_xfrm.go
index f9d9d24..932ef20 100644
--- a/net_xfrm.go
+++ b/net_xfrm.go
@@ -115,7 +115,7 @@ func (fs FS) NewXfrmStat() (XfrmStat, error) {
 		fields := strings.Fields(s.Text())
 
 		if len(fields) != 2 {
-			return XfrmStat{}, fmt.Errorf("couldn't parse %q line %q", file.Name(), s.Text())
+			return XfrmStat{}, fmt.Errorf("%w: %q line %q", ErrFileParse, file.Name(), s.Text())
 		}
 
 		name := fields[0]
diff --git a/proc.go b/proc.go
index 48f39da..e159996 100644
--- a/proc.go
+++ b/proc.go
@@ -15,6 +15,7 @@ package procfs
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -35,6 +36,12 @@ type Proc struct {
 // Procs represents a list of Proc structs.
 type Procs []Proc
 
+var (
+	ErrFileParse  = errors.New("Error Parsing File")
+	ErrFileRead   = errors.New("Error Reading File")
+	ErrMountPoint = errors.New("Error Accessing Mount point")
+)
+
 func (p Procs) Len() int           { return len(p) }
 func (p Procs) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
 func (p Procs) Less(i, j int) bool { return p[i].PID < p[j].PID }
@@ -42,7 +49,7 @@ func (p Procs) Less(i, j int) bool { return p[i].PID < p[j].PID }
 // Self returns a process for the current process read via /proc/self.
 func Self() (Proc, error) {
 	fs, err := NewFS(DefaultMountPoint)
-	if err != nil {
+	if err != nil || errors.Unwrap(err) == ErrMountPoint {
 		return Proc{}, err
 	}
 	return fs.Self()
@@ -104,7 +111,7 @@ func (fs FS) AllProcs() (Procs, error) {
 
 	names, err := d.Readdirnames(-1)
 	if err != nil {
-		return Procs{}, fmt.Errorf("could not read %q: %w", d.Name(), err)
+		return Procs{}, fmt.Errorf("%s: Cannot read file: %v: %w", ErrFileRead, names, err)
 	}
 
 	p := Procs{}
@@ -205,7 +212,7 @@ func (p Proc) FileDescriptors() ([]uintptr, error) {
 	for i, n := range names {
 		fd, err := strconv.ParseInt(n, 10, 32)
 		if err != nil {
-			return nil, fmt.Errorf("could not parse fd %q: %w", n, err)
+			return nil, fmt.Errorf("%s: Cannot parse line: %v: %w", ErrFileParse, i, err)
 		}
 		fds[i] = uintptr(fd)
 	}
@@ -290,7 +297,7 @@ func (p Proc) fileDescriptors() ([]string, error) {
 
 	names, err := d.Readdirnames(-1)
 	if err != nil {
-		return nil, fmt.Errorf("could not read %q: %w", d.Name(), err)
+		return nil, fmt.Errorf("%s: Cannot read file: %v: %w", ErrFileRead, names, err)
 	}
 
 	return names, nil
diff --git a/proc_cgroup.go b/proc_cgroup.go
index ea83a75..daeed7f 100644
--- a/proc_cgroup.go
+++ b/proc_cgroup.go
@@ -51,7 +51,7 @@ func parseCgroupString(cgroupStr string) (*Cgroup, error) {
 
 	fields := strings.SplitN(cgroupStr, ":", 3)
 	if len(fields) < 3 {
-		return nil, fmt.Errorf("at least 3 fields required, found %d fields in cgroup string: %s", len(fields), cgroupStr)
+		return nil, fmt.Errorf("%w: 3+ fields required, found %d fields in cgroup string: %s", ErrFileParse, len(fields), cgroupStr)
 	}
 
 	cgroup := &Cgroup{
@@ -60,7 +60,7 @@ func parseCgroupString(cgroupStr string) (*Cgroup, error) {
 	}
 	cgroup.HierarchyID, err = strconv.Atoi(fields[0])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse hierarchy ID")
+		return nil, fmt.Errorf("%w: hierarchy ID: %q", ErrFileParse, cgroup.HierarchyID)
 	}
 	if fields[1] != "" {
 		ssNames := strings.Split(fields[1], ",")
diff --git a/proc_cgroups.go b/proc_cgroups.go
index 24d4dce..5dd4938 100644
--- a/proc_cgroups.go
+++ b/proc_cgroups.go
@@ -46,7 +46,7 @@ func parseCgroupSummaryString(CgroupSummaryStr string) (*CgroupSummary, error) {
 	fields := strings.Fields(CgroupSummaryStr)
 	// require at least 4 fields
 	if len(fields) < 4 {
-		return nil, fmt.Errorf("at least 4 fields required, found %d fields in cgroup info string: %s", len(fields), CgroupSummaryStr)
+		return nil, fmt.Errorf("%w: 4+ fields required, found %d fields in cgroup info string: %s", ErrFileParse, len(fields), CgroupSummaryStr)
 	}
 
 	CgroupSummary := &CgroupSummary{
@@ -54,15 +54,15 @@ func parseCgroupSummaryString(CgroupSummaryStr string) (*CgroupSummary, error) {
 	}
 	CgroupSummary.Hierarchy, err = strconv.Atoi(fields[1])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse hierarchy ID")
+		return nil, fmt.Errorf("%w: Unable to parse hierarchy ID from %q", ErrFileParse, fields[1])
 	}
 	CgroupSummary.Cgroups, err = strconv.Atoi(fields[2])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse Cgroup Num")
+		return nil, fmt.Errorf("%w: Unable to parse Cgroup Num from %q", ErrFileParse, fields[2])
 	}
 	CgroupSummary.Enabled, err = strconv.Atoi(fields[3])
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse Enabled")
+		return nil, fmt.Errorf("%w: Unable to parse Enabled from %q", ErrFileParse, fields[3])
 	}
 	return CgroupSummary, nil
 }
diff --git a/proc_fdinfo.go b/proc_fdinfo.go
index 1bbdd4a..4b7933e 100644
--- a/proc_fdinfo.go
+++ b/proc_fdinfo.go
@@ -111,7 +111,7 @@ func parseInotifyInfo(line string) (*InotifyInfo, error) {
 		}
 		return i, nil
 	}
-	return nil, fmt.Errorf("invalid inode entry: %q", line)
+	return nil, fmt.Errorf("%w: invalid inode entry: %q", ErrFileParse, line)
 }
 
 // ProcFDInfos represents a list of ProcFDInfo structs.
diff --git a/proc_interrupts.go b/proc_interrupts.go
index 9df79c2..86b4b45 100644
--- a/proc_interrupts.go
+++ b/proc_interrupts.go
@@ -66,7 +66,7 @@ func parseInterrupts(r io.Reader) (Interrupts, error) {
 			continue
 		}
 		if len(parts) < 2 {
-			return nil, fmt.Errorf("not enough fields in interrupts (expected at least 2 fields but got %d): %s", len(parts), parts)
+			return nil, fmt.Errorf("%w: Not enough fields in interrupts (expected 2+ fields but got %d): %s", ErrFileParse, len(parts), parts)
 		}
 		intName := parts[0][:len(parts[0])-1] // remove trailing :
 
diff --git a/proc_limits.go b/proc_limits.go
index 7a13881..c86d815 100644
--- a/proc_limits.go
+++ b/proc_limits.go
@@ -103,7 +103,7 @@ func (p Proc) Limits() (ProcLimits, error) {
 		//fields := limitsMatch.Split(s.Text(), limitsFields)
 		fields := limitsMatch.FindStringSubmatch(s.Text())
 		if len(fields) != limitsFields {
-			return ProcLimits{}, fmt.Errorf("couldn't parse %q line %q", f.Name(), s.Text())
+			return ProcLimits{}, fmt.Errorf("%w: couldn't parse %q line %q", ErrFileParse, f.Name(), s.Text())
 		}
 
 		switch fields[1] {
@@ -154,7 +154,7 @@ func parseUint(s string) (uint64, error) {
 	}
 	i, err := strconv.ParseUint(s, 10, 64)
 	if err != nil {
-		return 0, fmt.Errorf("couldn't parse value %q: %w", s, err)
+		return 0, fmt.Errorf("%s: couldn't parse value %q: %w", ErrFileParse, s, err)
 	}
 	return i, nil
 }
diff --git a/proc_maps.go b/proc_maps.go
index f1bcbf3..727549a 100644
--- a/proc_maps.go
+++ b/proc_maps.go
@@ -65,7 +65,7 @@ type ProcMap struct {
 func parseDevice(s string) (uint64, error) {
 	toks := strings.Split(s, ":")
 	if len(toks) < 2 {
-		return 0, fmt.Errorf("unexpected number of fields")
+		return 0, fmt.Errorf("%w: unexpected number of fields, expected: 2, got: %q", ErrFileParse, len(toks))
 	}
 
 	major, err := strconv.ParseUint(toks[0], 16, 0)
@@ -95,7 +95,7 @@ func parseAddress(s string) (uintptr, error) {
 func parseAddresses(s string) (uintptr, uintptr, error) {
 	toks := strings.Split(s, "-")
 	if len(toks) < 2 {
-		return 0, 0, fmt.Errorf("invalid address")
+		return 0, 0, fmt.Errorf("%w: invalid address", ErrFileParse)
 	}
 
 	saddr, err := parseAddress(toks[0])
@@ -114,7 +114,7 @@ func parseAddresses(s string) (uintptr, uintptr, error) {
 // parsePermissions parses a token and returns any that are set.
 func parsePermissions(s string) (*ProcMapPermissions, error) {
 	if len(s) < 4 {
-		return nil, fmt.Errorf("invalid permissions token")
+		return nil, fmt.Errorf("%w: invalid permissions token", ErrFileParse)
 	}
 
 	perms := ProcMapPermissions{}
@@ -141,7 +141,7 @@ func parsePermissions(s string) (*ProcMapPermissions, error) {
 func parseProcMap(text string) (*ProcMap, error) {
 	fields := strings.Fields(text)
 	if len(fields) < 5 {
-		return nil, fmt.Errorf("truncated procmap entry")
+		return nil, fmt.Errorf("%w: truncated procmap entry", ErrFileParse)
 	}
 
 	saddr, eaddr, err := parseAddresses(fields[0])
diff --git a/proc_netstat.go b/proc_netstat.go
index 6a43bb2..8e3ff4d 100644
--- a/proc_netstat.go
+++ b/proc_netstat.go
@@ -195,8 +195,8 @@ func parseProcNetstat(r io.Reader, fileName string) (ProcNetstat, error) {
 		// Remove trailing :.
 		protocol := strings.TrimSuffix(nameParts[0], ":")
 		if len(nameParts) != len(valueParts) {
-			return procNetstat, fmt.Errorf("mismatch field count mismatch in %s: %s",
-				fileName, protocol)
+			return procNetstat, fmt.Errorf("%w: mismatch field count mismatch in %s: %s",
+				ErrFileParse, fileName, protocol)
 		}
 		for i := 1; i < len(nameParts); i++ {
 			value, err := strconv.ParseFloat(valueParts[i], 64)
diff --git a/proc_ns.go b/proc_ns.go
index 391b4cb..c226667 100644
--- a/proc_ns.go
+++ b/proc_ns.go
@@ -40,7 +40,7 @@ func (p Proc) Namespaces() (Namespaces, error) {
 
 	names, err := d.Readdirnames(-1)
 	if err != nil {
-		return nil, fmt.Errorf("failed to read contents of ns dir: %w", err)
+		return nil, fmt.Errorf("%s: failed to read contents of ns dir: %w", ErrFileRead, err)
 	}
 
 	ns := make(Namespaces, len(names))
@@ -52,13 +52,13 @@ func (p Proc) Namespaces() (Namespaces, error) {
 
 		fields := strings.SplitN(target, ":", 2)
 		if len(fields) != 2 {
-			return nil, fmt.Errorf("failed to parse namespace type and inode from %q", target)
+			return nil, fmt.Errorf("%w: namespace type and inode from %q", ErrFileParse, target)
 		}
 
 		typ := fields[0]
 		inode, err := strconv.ParseUint(strings.Trim(fields[1], "[]"), 10, 32)
 		if err != nil {
-			return nil, fmt.Errorf("failed to parse inode from %q: %w", fields[1], err)
+			return nil, fmt.Errorf("%s: inode from %q: %w", ErrFileParse, fields[1], err)
 		}
 
 		ns[name] = Namespace{typ, uint32(inode)}
diff --git a/proc_psi.go b/proc_psi.go
index a68fe15..152539d 100644
--- a/proc_psi.go
+++ b/proc_psi.go
@@ -61,7 +61,7 @@ type PSIStats struct {
 func (fs FS) PSIStatsForResource(resource string) (PSIStats, error) {
 	data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%s/%s", "pressure", resource)))
 	if err != nil {
-		return PSIStats{}, fmt.Errorf("psi_stats: unavailable for %q: %w", resource, err)
+		return PSIStats{}, fmt.Errorf("%s: psi_stats: unavailable for %q: %w", ErrFileRead, resource, err)
 	}
 
 	return parsePSIStats(resource, bytes.NewReader(data))
diff --git a/proc_snmp.go b/proc_snmp.go
index 6c46b71..b9d2cf6 100644
--- a/proc_snmp.go
+++ b/proc_snmp.go
@@ -159,8 +159,8 @@ func parseSnmp(r io.Reader, fileName string) (ProcSnmp, error) {
 		// Remove trailing :.
 		protocol := strings.TrimSuffix(nameParts[0], ":")
 		if len(nameParts) != len(valueParts) {
-			return procSnmp, fmt.Errorf("mismatch field count mismatch in %s: %s",
-				fileName, protocol)
+			return procSnmp, fmt.Errorf("%w: mismatch field count mismatch in %s: %s",
+				ErrFileParse, fileName, protocol)
 		}
 		for i := 1; i < len(nameParts); i++ {
 			value, err := strconv.ParseFloat(valueParts[i], 64)
diff --git a/proc_stat.go b/proc_stat.go
index 14b249f..923e550 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -138,7 +138,7 @@ func (p Proc) Stat() (ProcStat, error) {
 	)
 
 	if l < 0 || r < 0 {
-		return ProcStat{}, fmt.Errorf("unexpected format, couldn't extract comm %q", data)
+		return ProcStat{}, fmt.Errorf("%w: unexpected format, couldn't extract comm %q", ErrFileParse, data)
 	}
 
 	s.Comm = string(data[l+1 : r])
diff --git a/proc_sys.go b/proc_sys.go
index d46533e..12c5bf0 100644
--- a/proc_sys.go
+++ b/proc_sys.go
@@ -44,7 +44,7 @@ func (fs FS) SysctlInts(sysctl string) ([]int, error) {
 		vp := util.NewValueParser(f)
 		values[i] = vp.Int()
 		if err := vp.Err(); err != nil {
-			return nil, fmt.Errorf("field %d in sysctl %s is not a valid int: %w", i, sysctl, err)
+			return nil, fmt.Errorf("%s: field %d in sysctl %s is not a valid int: %w", ErrFileParse, i, sysctl, err)
 		}
 	}
 	return values, nil
diff --git a/slab.go b/slab.go
index bc9aaf5..8611c90 100644
--- a/slab.go
+++ b/slab.go
@@ -68,7 +68,7 @@ func parseV21SlabEntry(line string) (*Slab, error) {
 	l := slabSpace.ReplaceAllString(line, " ")
 	s := strings.Split(l, " ")
 	if len(s) != 16 {
-		return nil, fmt.Errorf("unable to parse: %q", line)
+		return nil, fmt.Errorf("%w: unable to parse: %q", ErrFileParse, line)
 	}
 	var err error
 	i := &Slab{Name: s[0]}
diff --git a/softirqs.go b/softirqs.go
index 559129c..b8fad67 100644
--- a/softirqs.go
+++ b/softirqs.go
@@ -57,7 +57,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 	)
 
 	if !scanner.Scan() {
-		return Softirqs{}, fmt.Errorf("softirqs empty")
+		return Softirqs{}, fmt.Errorf("%w: softirqs empty", ErrFileRead)
 	}
 
 	for scanner.Scan() {
@@ -74,7 +74,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.Hi = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.Hi[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (HI%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (HI%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "TIMER:":
@@ -82,7 +82,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.Timer = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.Timer[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (TIMER%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (TIMER%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "NET_TX:":
@@ -90,7 +90,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.NetTx = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.NetTx[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (NET_TX%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (NET_TX%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "NET_RX:":
@@ -98,7 +98,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.NetRx = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.NetRx[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (NET_RX%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (NET_RX%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "BLOCK:":
@@ -106,7 +106,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.Block = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.Block[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (BLOCK%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (BLOCK%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "IRQ_POLL:":
@@ -114,7 +114,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.IRQPoll = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.IRQPoll[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (IRQ_POLL%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (IRQ_POLL%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "TASKLET:":
@@ -122,7 +122,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.Tasklet = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.Tasklet[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (TASKLET%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (TASKLET%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "SCHED:":
@@ -130,7 +130,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.Sched = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.Sched[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (SCHED%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (SCHED%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "HRTIMER:":
@@ -138,7 +138,7 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.HRTimer = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.HRTimer[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (HRTIMER%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (HRTIMER%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "RCU:":
@@ -146,14 +146,14 @@ func parseSoftirqs(r io.Reader) (Softirqs, error) {
 			softirqs.RCU = make([]uint64, len(perCPU))
 			for i, count := range perCPU {
 				if softirqs.RCU[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Softirqs{}, fmt.Errorf("couldn't parse %q (RCU%d): %w", count, i, err)
+					return Softirqs{}, fmt.Errorf("%s: couldn't parse %q (RCU%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		}
 	}
 
 	if err := scanner.Err(); err != nil {
-		return Softirqs{}, fmt.Errorf("couldn't parse softirqs: %w", err)
+		return Softirqs{}, fmt.Errorf("%s: couldn't parse softirqs: %w", ErrFileParse, err)
 	}
 
 	return softirqs, scanner.Err()
diff --git a/stat.go b/stat.go
index 586af48..05269c8 100644
--- a/stat.go
+++ b/stat.go
@@ -93,10 +93,10 @@ func parseCPUStat(line string) (CPUStat, int64, error) {
 		&cpuStat.Guest, &cpuStat.GuestNice)
 
 	if err != nil && err != io.EOF {
-		return CPUStat{}, -1, fmt.Errorf("couldn't parse %q (cpu): %w", line, err)
+		return CPUStat{}, -1, fmt.Errorf("%s: couldn't parse %q (cpu): %w", ErrFileParse, line, err)
 	}
 	if count == 0 {
-		return CPUStat{}, -1, fmt.Errorf("couldn't parse %q (cpu): 0 elements parsed", line)
+		return CPUStat{}, -1, fmt.Errorf("%w: couldn't parse %q (cpu): 0 elements parsed", ErrFileParse, line)
 	}
 
 	cpuStat.User /= userHZ
@@ -116,7 +116,7 @@ func parseCPUStat(line string) (CPUStat, int64, error) {
 
 	cpuID, err := strconv.ParseInt(cpu[3:], 10, 64)
 	if err != nil {
-		return CPUStat{}, -1, fmt.Errorf("couldn't parse %q (cpu/cpuid): %w", line, err)
+		return CPUStat{}, -1, fmt.Errorf("%s: couldn't parse %q (cpu/cpuid): %w", ErrFileParse, line, err)
 	}
 
 	return cpuStat, cpuID, nil
@@ -136,7 +136,7 @@ func parseSoftIRQStat(line string) (SoftIRQStat, uint64, error) {
 		&softIRQStat.Hrtimer, &softIRQStat.Rcu)
 
 	if err != nil {
-		return SoftIRQStat{}, 0, fmt.Errorf("couldn't parse %q (softirq): %w", line, err)
+		return SoftIRQStat{}, 0, fmt.Errorf("%s: couldn't parse %q (softirq): %w", ErrFileParse, line, err)
 	}
 
 	return softIRQStat, total, nil
@@ -197,34 +197,34 @@ func parseStat(r io.Reader, fileName string) (Stat, error) {
 		switch {
 		case parts[0] == "btime":
 			if stat.BootTime, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (btime): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (btime): %w", ErrFileParse, parts[1], err)
 			}
 		case parts[0] == "intr":
 			if stat.IRQTotal, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (intr): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (intr): %w", ErrFileParse, parts[1], err)
 			}
 			numberedIRQs := parts[2:]
 			stat.IRQ = make([]uint64, len(numberedIRQs))
 			for i, count := range numberedIRQs {
 				if stat.IRQ[i], err = strconv.ParseUint(count, 10, 64); err != nil {
-					return Stat{}, fmt.Errorf("couldn't parse %q (intr%d): %w", count, i, err)
+					return Stat{}, fmt.Errorf("%s: couldn't parse %q (intr%d): %w", ErrFileParse, count, i, err)
 				}
 			}
 		case parts[0] == "ctxt":
 			if stat.ContextSwitches, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (ctxt): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (ctxt): %w", ErrFileParse, parts[1], err)
 			}
 		case parts[0] == "processes":
 			if stat.ProcessCreated, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (processes): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (processes): %w", ErrFileParse, parts[1], err)
 			}
 		case parts[0] == "procs_running":
 			if stat.ProcessesRunning, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (procs_running): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (procs_running): %w", ErrFileParse, parts[1], err)
 			}
 		case parts[0] == "procs_blocked":
 			if stat.ProcessesBlocked, err = strconv.ParseUint(parts[1], 10, 64); err != nil {
-				return Stat{}, fmt.Errorf("couldn't parse %q (procs_blocked): %w", parts[1], err)
+				return Stat{}, fmt.Errorf("%s: couldn't parse %q (procs_blocked): %w", ErrFileParse, parts[1], err)
 			}
 		case parts[0] == "softirq":
 			softIRQStats, total, err := parseSoftIRQStat(line)
@@ -247,7 +247,7 @@ func parseStat(r io.Reader, fileName string) (Stat, error) {
 	}
 
 	if err := scanner.Err(); err != nil {
-		return Stat{}, fmt.Errorf("couldn't parse %q: %w", fileName, err)
+		return Stat{}, fmt.Errorf("%s: couldn't parse %q: %w", ErrFileParse, fileName, err)
 	}
 
 	return stat, nil
diff --git a/swaps.go b/swaps.go
index 15edc22..fa00f55 100644
--- a/swaps.go
+++ b/swaps.go
@@ -64,7 +64,7 @@ func parseSwapString(swapString string) (*Swap, error) {
 	swapFields := strings.Fields(swapString)
 	swapLength := len(swapFields)
 	if swapLength < 5 {
-		return nil, fmt.Errorf("too few fields in swap string: %s", swapString)
+		return nil, fmt.Errorf("%w: too few fields in swap string: %s", ErrFileParse, swapString)
 	}
 
 	swap := &Swap{
@@ -74,15 +74,15 @@ func parseSwapString(swapString string) (*Swap, error) {
 
 	swap.Size, err = strconv.Atoi(swapFields[2])
 	if err != nil {
-		return nil, fmt.Errorf("invalid swap size: %s", swapFields[2])
+		return nil, fmt.Errorf("%s: invalid swap size: %s: %w", ErrFileParse, swapFields[2], err)
 	}
 	swap.Used, err = strconv.Atoi(swapFields[3])
 	if err != nil {
-		return nil, fmt.Errorf("invalid swap used: %s", swapFields[3])
+		return nil, fmt.Errorf("%s: invalid swap used: %s: %w", ErrFileParse, swapFields[3], err)
 	}
 	swap.Priority, err = strconv.Atoi(swapFields[4])
 	if err != nil {
-		return nil, fmt.Errorf("invalid swap priority: %s", swapFields[4])
+		return nil, fmt.Errorf("%s: invalid swap priority: %s: %w", ErrFileParse, swapFields[4], err)
 	}
 
 	return swap, nil
diff --git a/sysfs/system_cpu.go b/sysfs/system_cpu.go
index 9e15858..fbecc19 100644
--- a/sysfs/system_cpu.go
+++ b/sysfs/system_cpu.go
@@ -170,19 +170,20 @@ func binSearch(elem uint16, elemSlice *[]uint16) bool {
 	return false
 }
 
-func filterOfflineCPUs(offlineCpus *[]uint16, cpus *[]string) error {
-	for i, cpu := range *cpus {
+func filterOfflineCPUs(offlineCpus *[]uint16, cpus *[]string) ([]string, error) {
+	var filteredCPUs []string
+	for _, cpu := range *cpus {
 		cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu")
 		cpuNameUint16, err := strconv.Atoi(cpuName)
 		if err != nil {
-			return err
+			return nil, err
 		}
-		if binSearch(uint16(cpuNameUint16), offlineCpus) {
-			*cpus = append((*cpus)[:i], (*cpus)[i+1:]...)
+		if !binSearch(uint16(cpuNameUint16), offlineCpus) {
+			filteredCPUs = append(filteredCPUs, cpu)
 		}
 	}
 
-	return nil
+	return filteredCPUs, nil
 }
 
 // SystemCpufreq returns CPU frequency metrics for all CPUs.
@@ -206,7 +207,7 @@ func (fs FS) SystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 		}
 
 		if len(offlineCPUs) > 0 {
-			err = filterOfflineCPUs(&offlineCPUs, &cpus)
+			cpus, err = filterOfflineCPUs(&offlineCPUs, &cpus)
 			if err != nil {
 				return nil, err
 			}
diff --git a/thread.go b/thread.go
index 490c147..394762c 100644
--- a/thread.go
+++ b/thread.go
@@ -45,7 +45,7 @@ func (fs FS) AllThreads(pid int) (Procs, error) {
 
 	names, err := d.Readdirnames(-1)
 	if err != nil {
-		return Procs{}, fmt.Errorf("could not read %q: %w", d.Name(), err)
+		return Procs{}, fmt.Errorf("%s: could not read %q: %w", ErrFileRead, d.Name(), err)
 	}
 
 	t := Procs{}
diff --git a/vm.go b/vm.go
index cdedcae..51c49d8 100644
--- a/vm.go
+++ b/vm.go
@@ -86,7 +86,7 @@ func (fs FS) VM() (*VM, error) {
 		return nil, err
 	}
 	if !file.Mode().IsDir() {
-		return nil, fmt.Errorf("%s is not a directory", path)
+		return nil, fmt.Errorf("%w: %s is not a directory", ErrFileRead, path)
 	}
 
 	files, err := os.ReadDir(path)
diff --git a/zoneinfo.go b/zoneinfo.go
index c745a4c..ce5fefa 100644
--- a/zoneinfo.go
+++ b/zoneinfo.go
@@ -75,11 +75,11 @@ var nodeZoneRE = regexp.MustCompile(`(\d+), zone\s+(\w+)`)
 func (fs FS) Zoneinfo() ([]Zoneinfo, error) {
 	data, err := os.ReadFile(fs.proc.Path("zoneinfo"))
 	if err != nil {
-		return nil, fmt.Errorf("error reading zoneinfo %q: %w", fs.proc.Path("zoneinfo"), err)
+		return nil, fmt.Errorf("%s: error reading zoneinfo %q: %w", ErrFileRead, fs.proc.Path("zoneinfo"), err)
 	}
 	zoneinfo, err := parseZoneinfo(data)
 	if err != nil {
-		return nil, fmt.Errorf("error parsing zoneinfo %q: %w", fs.proc.Path("zoneinfo"), err)
+		return nil, fmt.Errorf("%s: error parsing zoneinfo %q: %w", ErrFileParse, fs.proc.Path("zoneinfo"), err)
 	}
 	return zoneinfo, nil
 }


Test patch:
diff --git a/buddyinfo_test.go b/buddyinfo_test.go
index 002e3be..8237724 100644
--- a/buddyinfo_test.go
+++ b/buddyinfo_test.go
@@ -52,8 +52,8 @@ Node 0, zone
 	if err == nil {
 		t.Fatalf("expected error, but none occurred")
 	}
-	if want, got := "invalid number of fields when parsing buddyinfo", err.Error(); want != got {
-		t.Fatalf("wrong error returned, wanted %q, got %q", want, got)
+	if want, got := "Error Parsing File: Invalid number of fields, found: [Node 0, zone]", err.Error(); want != got {
+		t.Fatalf("Error Parsing File: Invalid number of fields, found: [Node %q, %q]", want, got)
 	}
 }
 
@@ -68,7 +68,7 @@ Node 0, zone   Normal   4381   1093    185   1530    567    102      4      0
 	if err == nil {
 		t.Fatalf("expected error, but none occurred")
 	}
-	if want, got := "mismatch in number of buddyinfo buckets", err.Error(); !strings.HasPrefix(got, want) {
-		t.Fatalf("wrong error returned, wanted prefix %q, got %q", want, got)
+	if want, got := "Error Parsing File: mismatch in number of buddyinfo buckets, previous count 11, new count 12", err.Error(); !strings.HasPrefix(got, want) {
+		t.Fatalf("Error Parsing File: mismatch in number of buddyinfo buckets, previous count %q, new count %q", want, got)
 	}
 }
diff --git a/net_route_test.go b/net_route_test.go
new file mode 100644
index 0000000..0464036
--- /dev/null
+++ b/net_route_test.go
@@ -0,0 +1,60 @@
+// Copyright 2023 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+)
+
+func TestParseNetRoute(t *testing.T) {
+	var netRoute = []byte(`Iface            Destination  Gateway   Flags  RefCnt  Use  Metric  Mask      MTU  Window  IRTT
+eno16780032      00000000     9503A8C0  0003   0       0    100     00000000  0    0       0
+eno16780032      0000A8C0     00000000  0001   0       0    100     0000FFFF  0    0       0`)
+
+	r := bytes.NewReader(netRoute)
+	parsed, _ := parseNetRoute(r)
+	want := []NetRouteLine{
+		{
+			Iface:       "eno16780032",
+			Destination: 0,
+			Gateway:     2500044992,
+			Flags:       3,
+			RefCnt:      0,
+			Use:         0,
+			Metric:      100,
+			Mask:        0,
+			MTU:         0,
+			Window:      0,
+			IRTT:        0,
+		},
+		{
+			Iface:       "eno16780032",
+			Destination: 43200,
+			Gateway:     0,
+			Flags:       1,
+			RefCnt:      0,
+			Use:         0,
+			Metric:      100,
+			Mask:        65535,
+			MTU:         0,
+			Window:      0,
+			IRTT:        0,
+		},
+	}
+	if !reflect.DeepEqual(want, parsed) {
+		t.Errorf("want %v, parsed %v", want, parsed)
+	}
+}
diff --git a/proc_sys_test.go b/proc_sys_test.go
index 2f8b867..6059390 100644
--- a/proc_sys_test.go
+++ b/proc_sys_test.go
@@ -69,12 +69,12 @@ func TestSysctlIntsError(t *testing.T) {
 		sysctl string
 		want   string
 	}{
-		{"kernel.seccomp.actions_avail", "field 0 in sysctl kernel.seccomp.actions_avail is not a valid int: strconv.ParseInt: parsing \"kill_process\": invalid syntax"},
+		{"kernel.seccomp.actions_avail", "Error Parsing File: field 0 in sysctl kernel.seccomp.actions_avail is not a valid int: strconv.ParseInt: parsing \"kill_process\": invalid syntax"},
 	} {
 		t.Run(tc.sysctl, func(t *testing.T) {
 			_, err := fs.SysctlInts(tc.sysctl)
 			if err == nil {
-				t.Fatal("expected error")
+				t.Fatal("Error Parsing File: expected error")
 			}
 			if diff := cmp.Diff(tc.want, err.Error()); diff != "" {
 				t.Fatalf("unexpected syscall value(-want +got):\n%s", diff)
diff --git a/sysfs/system_cpu_test.go b/sysfs/system_cpu_test.go
index 864e2ea..9fee2a1 100644
--- a/sysfs/system_cpu_test.go
+++ b/sysfs/system_cpu_test.go
@@ -35,7 +35,7 @@ func TestCPUTopology(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if want, have := 2, len(cpus); want != have {
+	if want, have := 3, len(cpus); want != have {
 		t.Errorf("incorrect number of CPUs, have %v, want %v", want, have)
 	}
 	if want, have := "0", cpus[0].Number(); want != have {
@@ -72,7 +72,7 @@ func TestCPUThermalThrottle(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if want, have := 2, len(cpus); want != have {
+	if want, have := 3, len(cpus); want != have {
 		t.Errorf("incorrect number of CPUs, have %v, want %v", want, have)
 	}
 	cpu0Throttle, err := cpus[0].ThermalThrottle()
diff --git a/testdata/fixtures.ttar b/testdata/fixtures.ttar
index fd2b9e5..cc2ff45 100644
--- a/testdata/fixtures.ttar
+++ b/testdata/fixtures.ttar
@@ -13152,6 +13152,9 @@ Lines: 1
 1,5
 Mode: 444
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Directory: fixtures/sys/devices/system/cpu/cpu2
+Mode: 775
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/cpu/cpufreq
 Mode: 775
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -13228,7 +13231,7 @@ Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/sys/devices/system/cpu/offline
 Lines: 1
-2,12-15
+2
 Mode: 664
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Directory: fixtures/sys/devices/system/node