I have some C and some Odin experience, though not as a professional programmer. I want to try out Zig as well, and this ! operator caught my attention. As far as I know, it’s a way to tell the compiler, that my function either returns the return type or an error. I can vaguley see why someone want to do that, but can’t imagine how should I want to do that. Why can I sometime leave out the ! operator in main function? In the documentation at the Hello World example, first we use it, than omit it, as
" the ! may be omitted from the return type because no errors are returned from the function."
Wouldn’t it be helpful to leave the ! there so later if I change something in the program it may caught some error. And why exactly I want to caught an error this way? If something is wrong the program will crash no? So shouldn’t it be appropriate to use it, when something is not comptime?
What about the test operation, why shouldn’t I just use that? Is there any benefit to use both of them?
The main function is special because it interacts with the operating system. The return type of the main function is an approximation of how your program will interact with the OS.
If you return an error from the main function, your process with exit with a non-zero exit code, indicating an error to the OS and user.
If you do not desire to tell this to the user, for example, if you know your program will always succeed, feel free to omit the !.
You can choose what you want to happen! For example, would you want your database to kill itself if someone sent a malformed query? Probably not. So the language is providing you some features to help you handle errors, or not.
Tests are primarily to help you develop your software, not let a user run it. Tests help you ensure that your software behaves correctly. Typically a user will get a binary from you (like a game executable). Tests help you ensure that the software you write won’t crash or have bugs before you ship the executable to your users.
Typically you put the tests in your build script so you can run them many times while you are writing your code.
One of the biggest use cases for not having an errable function is resource clean up. Zig zen states that “Resource allocation may fail; resource deallocation must succeed.” In order to uphold this ideal, there’s friction built-in to the defer mechanic preventing the use of the try keyword. In these cases you MUST handle any errors within the defer block. This is doable with catch for sure, but it makes more sense to have resource deallocation functions that don’t fail so they can be easily called from higher up.
If you’d like to see examples of this look at functions such as std.mem.Allocator.free() and the various deinit() methods within the standard library.
This is just one example of reasons not to keep the ! in the Return Type, but it’s the one that clicked for me earliest on. Hope that helps and welcome to the community!
(pedantic warning):
The exclamation point ! to indicate error returns is not an operator – it’s just part of the syntax, like semicolons, brackets, and parentheses.
More or less it depends on how you handle errors. If a function can return an error, the caller must handle that error. One way is to let the error propagate along the way (fn caller() !something {…}), another way is to properly handle the error in place (fn caller() something {…}).
Thanks, I realizes I have mislead some of you with the main function example, as I just used it as an example, but wanted to know more about the ! sybmol.
Your previous comment helped, thanks!
I made a little example of the different things you can do with the ! symbol, but basically it gives you a lot of freedom in how you want to express error handling, It’s better if you can explicitly state what kind of errors your function can return but since you sometimes don’t know for sure it’s a handy tool.
const std = @import("std");
const FooError = error{
Recoverable,
Unrecoverable,
};
fn failingFoo() !void {
return if (@rem(std.time.timestamp(), 2) == 0) FooError.Recoverable else FooError.Unrecoverable;
}
fn tryBar() !void {
std.log.info("I can let the errors bubble up to handle them elsewhere", .{});
try failingFoo();
}
fn catchBar() error{Unrecoverable}!void {
failingFoo() catch |err| {
if (error.Recoverable == err) std.log.info("I can handle some of the errors locally {!} and return the rest", .{err}) else return error.Unrecoverable;
};
}
fn superCatchBar() void {
failingFoo() catch |err| {
switch (err) {
error.Recoverable => std.log.info("I can choose to handle them all localy.", .{}),
error.Unrecoverable => std.log.info("even if they are meant to say something went really wrong", .{}),
}
};
}
pub fn main() anyerror!void {
{
superCatchBar();
}
{
catchBar() catch |err| {
std.log.err("something went wrong {!}", .{err});
};
}
{
tryBar() catch |err| {
if (err == FooError.Recoverable) {
std.log.info("I can also handle it here :{!}", .{err});
} else {
return err;
}
};
}
}