What is allowed inside a defer expression?

I’m very new to zig and found a hello world example that writes to stdout using a buffered writer ie: var bw = std.io.bufferedWriter(stdout_file);. At the end of the main function it has try bw.flush();.

I thought I’d try putting this inside a defer expression right underneath where bw is defined, but haven’t been able to make it work. If I write:
defer try bw.flush(); I get error: 'try' not allowed inside defer expression

If I write:
defer bw.flush(); I get error: error union ignored

Reading up on defer I can’t find an explanation for what is and isn’t allowed in a defer expression.

1 Like

You can’t have control flow inside a deferred statement. In this case try bw.flush() is short for bw.flush() catch |err| return err; and return is control flow. One way to think of this is that by the time a defer statement is running the scope has ended and the return value will have already been decided.

From the docs:

Inside a defer expression the return statement is not allowed.

5 Likes

If you don’t care about the flush failing, you can “handle” the error by ignoring it:

defer bw.flush() catch {};

or you can panic

defer bw.flush() catch @panic("Flush failed!");

So in the end, the fact that you can’t use try in the defer is good because it makes you decide how to handle an error when exiting a scope.

6 Likes

I thought that because this main function has return type !void that defer bw.flush() failing would fit with the type of the function. However if as you say the return value has already been decided, that’s more understandable.

You can’t have control flow inside a deferred statement.

That’s clumsy wording; you can absolutely do control flow, including calling functions, having conditionals, etc. What’s not allowed is for a deferred statement to contain control flow which exits the defer/errdefer; for instance, breaking from a block outside of the defer, or, as in this case, return/try.


The accepted solution is technically accurate, and the other comment answers the question posed in the thread title (but note the above clarification). However, I think these answers are, if not misleading, a little incomplete, because they omit a crucial correction: flush should not be used in a defer. The use case for defer is for something you want to happen on all exits from a scope; this is almost always related to releasing resources (freeing memory, closing a file handle, etc). (errdefer is “the same thing, except only when i exit the scope by returning an error”.) flush is not doing that. flush is a failable operation which only makes sense to attempt on the success path of your operation. Don’t defer bw.flush() catch ... or anything of the sort; it is correct to write this as a normal statement in your success path. It’s like how you wouldn’t try to write defer try w.writeByte('\n'); in a “write line” function, you’d just do it at the end!

10 Likes

There’s a related thread which goes into more detail about why you probably don’t want to flush from a defer, as well as why you can’t try from a defer.

It can be worthwhile to flush from a defer in cases where the output gives a nicer experience when an error is returned. But I agree, in general, flushing on successful completion is better.