How to handle errors with less nesting?

Hi all,

I’m curious if there’s a way to handle errors with less nesting. Suppose I have a thread with a run loop that needs to receive a message. The receive function can return an error or a struct. The struct has a pointer which is an optional. In order to handle this I end up with something like:

while(!isShuttingDown()) {
  if(mailbox.receive()) |message| {
    if(message.topic) |topic| {
      if(topic.handle(message)) |reply| {
        message.respond(reply);
      } else |err| {
        // logging
      }
    } else {
      // logging
    }
  } else |err| {
    // logging
  }
}

(Disregard the specifics of the API… this is a contrived example to illustrate the problem.)

What I’m wondering about is how to flatten this out so the logic is not as obscured by all the levels of nesting. In a different language I might use continue like this:

while(!isShuttingDown()) {
  var message = mailbox.receive();

  if( /* check if error */ ) {
    // logging
    continue;
  }

  if(message.topic == null) {
    // logging
    continue;
  }

  var reply = message.topic.handle(message);
 
  if( /* check if reply is error */ ) {
    // logging
    continue;
  }

  if(reply == null) {
    // logging
    continue;
  }

  message.respond(reply);
}

Obviously this is a matter of personal taste. You might prefer the nested version as more compact. I’d just like to know if it is possible to do something like the flatter version.

One challenge I see is checking for the errors. There is std.meta.isError to test for an error, but I don’t see how to either get just the error part of an error union or just the value part. It seems like using if with payload binding is the only way to deconstruct that union. At least, I haven’t been able to find some like “left” and “right” for taking it apart.

Another challenge is that, without non-if ways to unwrap optionals and error unions, I would need to add .? and “something for errors” in the dereference expressions. E.g., message.topic.handle would be message.?.topic.?.handle. Basically, payload binding seems to be the only way to tell the compiler that the value has been checked for existence and non-errorness.

This should help:

 const value = message orelse {
      std.log.err("no message", .{});
      continue; // or break, return...
  };

You can change your control flow in the orelse and it will unpack the value if it’s not null.

Same thing with catch:

 const value = message.topic.handle(message) catch {
      std.log.err("failed to handle something", .{});
      continue; // or break, return...
  };
5 Likes

Wow… I absolutely didn’t realize you could put control flow inside an orelse or catch block. That is very helpful!

Yup - no problem. It’s overlooked quite a bit. Don’t forget that you can capture the error payload with catch: Captures and Payloads

2 Likes