Enums are great at what they do: creating a simple integer type with specific values. Their main purpose is to choose one value out of many like enum Color { Red, Green, Blue }. But what if you have data attached to those choices? What if the data is one type for one choice and another type for another choice? What if there are two pieces of data to attach to one choice and only one for another? Today’s article shows a simple pattern you can use instead of enum in these cases to get a lot more flexibility and extensibility. Read on to see how!

Say you’ve got a Login function that logs a user into a server:

void Login(string username, string password, Action<bool> callback);

The callback parameter gets called with true upon success and false upon failure. You usually quickly realize that a simple bool is an insufficient way of expressing all the errors that could happen while logging in. This is where you switch to an enum:

enum LoginError
{
	Success,
	InvalidCredentials,
	AccountLocked,
	TemporarilyBanned,
	PermanentlyBanned,
}
 
void Login(string username, string password, Action<LoginError> callback);

That’s a lot better than a simple bool because we can specify just what kind of error happened rather than a simple “pass” or “fail”. This would probably help out error reporting to the user a lot. It’ll definitely help extensibility a lot since we can add more error types to the enum as we discover them without changing the function signature of the Login function.

Problems start to emerge with this simple enum when we want to attach data to these states. For InvalidCredentials, how many more attempts is the user allowed to make before their account is locked? For AccountLocked and TemporarilyBanned, how long will this be the case? For TemporarilyBanned and PermanentlyBanned, what was the reason for the ban?

Really, we want a class for each of these types so we have somewhere to put one or more pieces of data related to the error. Here’s how they might look for these errors:

class InvalidCredentials
{
	int RemainingAttempts;
}
 
class AccountLocked
{
	DateTime UnlockDate;
}
 
class TemporarilyBanned
{
	DateTime UnbanDate;
	string Reason;
}
 
class PermanentlyBanned
{
	string Reason;
}

You can probably think of tons of more fields to add—ban dates, account lock reason, credentials hints—and now there’s a really convenient place to put them. You might even make a class for the success case if you want to attach some data to that, too. The only problem is that the Login function’s callback callback parameter can’t have pass four different class types. You don’t know what kind of error you’ll get ahead of time, so you can’t even make four overloads of the function. Instead, we need to group them together using an empty interface that they all implement:

interface ILoginError
{
}
 
class InvalidCredentials : ILoginError
{
	int RemainingAttempts;
}
 
class AccountLocked : ILoginError
{
	DateTime UnlockDate;
}
 
class TemporarilyBanned : ILoginError
{
	DateTime UnbanDate;
	string Reason;
}
 
class PermanentlyBanned : ILoginError
{
	string Reason;
}

Now the Login function’s callback can just use ILoginError:

void Login(string username, string password, Action<ILoginError> callback);

Instead of specifying an enum value, create the error like so:

new PermanentlyBanned { Reason = "Cheating" };

When handling the error, you’ll need to check each possible cause just like with enum:

void HandleLogin(ILoginError error)
{
	if (error == null)
	{
		Debug.Log("Success");
		return;
	}
 
	var invalidCredentials = error as InvalidCredentials;
	if (invalidCredentials != null)
	{
		Debug.LogError(
			"Invalid credentials. You have " + invalidCredentials.RemainingAttempts + " more tries."
		);
	}
 
	var accountLocked = error as AccountLocked;
	if (accountLocked != null)
	{
		Debug.LogError(
			"Account locked until " + accountLocked.UnlockDate + "."
		);
	}
 
	var temporarilyBanned = error as TemporarilyBanned;
	if (temporarilyBanned != null)
	{
		Debug.LogError(
			"Banned until " + temporarilyBanned.UnbanDate + " for " + temporarilyBanned.Reason + "."
		);
	}
 
	var permanentlyBanned = error as PermanentBanned;
	if (permanentlyBanned != null)
	{
		Debug.LogError(
			"Banned for " + permanentlyBanned.Reason + "."
		);
	}
}

That’s really all there is to it. There are three levels of specifying errors here: bool, enum, class. The class approach is by far more flexible and extensible than bool or enum. It also takes the most work to set up, but that’s largely due to the extra data you want to specify. The same is true of enum and bool: it takes a lot more setup to specify the kinds of errors with enum than to just use true and false.

Which level is right for you? Only you can decide. Which do you use in your apps? Share your thoughts in the comments!