Expanded use of asserts

Do you use assertions in your code? What for? How do you handle release builds ?

There is a better way than NSAssert...

Assertions, when should I use them ?

Update

New version of assert macros and more reasoning behind it is available here

Assertions are one of the oldest ways of making sure that code works the way we think it does.

Which means we should be adding assertions on all assumptions we make.

I believe we should also use Assertions as contracts when designing our API's, e.g. I don't believe any expensive operation should happen if you don't care about results (completionBlock being only way to get results here), so I'll make sure that developer using my API cares.

- (void)expensiveOperationWithCompletion:(void(^)(NSError *, id))completion {
NSParameterAssert(completion != nil);
}

Most people would only use assertions when checking our own assumptions, but I'm big fan of "Crash Early, Crash Often" and so I tend to write more asserts than most people, one place when I add extra assertions is integration points, especially with data coming from server.

E.g. I'm expecting to get Dictionary inside Array not the other way around. If I crash I'll know straight away that backend developer has changed something and didn't notify me.

How should I handle user-facing release builds?

User should never see crash if possible, the only crash I can accept is when there is unavoidable data corruption if we don't crash, which is pretty much always avoidable, especially with my approach.

So how people handle release builds ? They are 2 camps here, some people strip asserts on release builds (Apple does that by default in Xcode), some people think you should have them on release as well (mikeash does that).

I think you should strip assertions on release builds, but I also think just striping them is wrong, especially with the extended way I'm using them. What if you assert when working with data coming from server and server changes ?

It's gonna crash because some of your code expected something else, this is very bad UIX.

How you could handle Assert on server integration parts?

One could advocate that you might solve this problem like this:

NSParameterAssert([dataFromServer isKindOfClass:[NSDictionary class]]);
if ([dataFromServer isKindOfClass:[NSDictionary class]]) {
	//! release code
}

By striping asserts in release you'd get just the if happening. I think code like this is hard to maintain, leads to a lot of repetitions, I also belive you should also be generating some errors in that if statement or you won't know that something is wrong in release build.

Now imagine that most integration code would require multiple asserts (easily 5+), that's a lot of code that you don't want to write.

How I want assertions to work ?

My preferred assertions should work as follows:

  • Crash on debug, crash early crash often. I want to know if there is any problem with my code.
  • Never crash on release, don't break code flow if something unexpected happens, even on asynchronous code.
  • Generate and log error automatically when using release builds.
  • Write as little code as possible.

For those reasons I've implemented my own macros for Assertions, that's how the simplest one looks like:

#define AssertTrueOrReturn(condition) do {\
NSAssert((condition), @"Invalid condition not satisfying: %s", #condition);\
    if(!(condition)) {
    	pixle_NSErrorMake([NSString stringWithFormat:@"Unexpected behaviour not satisfying: %s", #condition], kErrorCodeInternal, nil, _cmd); return;\
    } } while(0)

Let me explain how it works:

  • do {} while(0) is a common C trick to get compiler to accept ; at the end of macro without complaining.
  • we use NSAssert as our base assertions, works in debug, stripped in release.
  • if statement executes only on release (debug already crashed).
  • if release build has unexpected behaviour, it will generate error automatically from failed condition.
  • pixle_NSErrorMake is just helper method for error creation, which also logs that error.
  • code will return after logging that error.

But that's the simplest assertion macro I've implemented, one that's way more interesting to use is this one, shown here as usage example:

  AssertTrueOrReturnBlock(userInfo != nil, ^(NSError *err) {
    completionBlock(err, nil);
  });

This macro will assert code, call block with automatically generated error and then return from code scope. Which makes it work with async code.

Whole set of asserts are as follows:

  • AssertTrueOrReturn <- assert + return;
  • AssertTrueOrReturnNil <- assert + return nil;
  • AssertTrueOrReturnBlock <- assert + execute block + return;
  • AssertTrueOrReturnNilBlock <- assert + execute block + return nil;
  • AssertTrueOrReturnError <- assert + execute block + return error;

Conclusion

I made a library that you can use in your project: KZAsserts

You've successfully subscribed to Krzysztof Zabłocki
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.