This very much depends on the environment and language you are working in. (SE Stackexchange is overrun with Java programmers, and most of the answer demonstrate as much.)
There are several common techniques:
1. Return values
Go doesn't have automatically propagating errors. All error handling is explicitly handled by returning a result and an optional error.
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
In functional languages, there is often an Either/Result monad that is used for this.
2. Out parameter
C# has output parameters. For example int.TryParse. It returns the success/failure and modifies the argument to store the resulting value.
int number;
if (int.TryParse(text, out number)) {
Console.WriteLine("invalid");
}
Console.WriteLine(number);
C functions often do similar things, using pointers.
3. Errors in exceptional cases only
Conventional Java/C# wisdom is that errors are appropriate in "unusual" cases. This largely depends on the level you are working at.
A failure to establish a TCP connection might be an error. An failed remote authentication attempt (e.g. HTTP 401/403) might be an error. A failure to create a file due to a conflict might be an error.
try {
socket = serverSocket.accept();
socket.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
There is usually a taxonomy of errors (e.g. in Java, "errors" are severe, program-threatening events, "unchecked exceptions" are indication of programmer error, and "checked exceptions" are unusual but expected possibilities).
4. Errors generally
In Python, errors are an acceptable and idiomatic form of flow control, just as much as if-then for for.
try:
a = things[key]
except KeyError:
print('Missing')
else:
print(a)
I recommend finding the pattern for your language/ecosystem/project and sticking to that.