Exception handling within a constructor

16,463

Solution 1

Throwing an exception when appropriate is part of a constructor's job.

Let's consider why we have constructors at all.

One part is convenience in having a method that sets various properties, and perhaps does some more advanced initialisation work (e.g. a FileStream will actually access the relevant file). But then if it was really that convenient all the time we wouldn't sometimes find member initialisers convenient.

The main reason for constructors is so that we can maintain object invariants.

An object invariant is something we can say about an object at the beginning and end of every method call. (If it was designed for concurrent use we would even have invariants that held during method calls).

One of the invariants of the Uri class is that if IsAbsoluteUri is true, then Host will be string that is a valid host, (if IsAbsoluteUri is false Host might be a valid host if it's scheme-relative, or accessing it might cause an InvalidOperationException).

As such when I'm using an object of such a class and I've checked IsAbsoluteUri I know I can access Host without an exception. I also know that it will indeed be a hostname, rather than e.g. a short treatise on mediaeval and early-modern beliefs in the apotropaic qualities of bezoars.

Okay, so some code putting such a treatise in there isn't exactly likely, but code putting some sort of junk into an object certainly is.

Maintaining an invariant comes down to making sure that the combinations of values the object holds always make sense. That has to be done in any property-setter or method that mutates the object (or by making the object immutable, because you can never have an invalid change if you never have a change) and those that set values initially, which is to say in a constructor.

In a strongly typed language we get some of the checking from that type-safety (a number that must be between 0 and 15 will never be set to "Modern analysis has found that bezoars do indeed neutralise arsenic." because the compiler just won't let you do that.) but what about the rest?

Consider the constructors for List<T> that take arguments. One of them takes an integer, and sets the internal capacity accordingly, and the other an IEnumerable<T> that the list is filled with. The start of these two constructors are:

public List(int capacity)
{
    if (capacity < 0) throw new ArgumentOutOfRangeException("capacity", capacity, SR.ArgumentOutOfRange_NeedNonNegNum);

/* … */

public List(IEnumerable<T> collection)
{
    if (collection == null)
        throw new ArgumentNullException("collection");

So if you call new List<string>(-2) or new List<int>(null) you get an exception, rather than an invalid list.

An interesting thing to note about this case, is that this is a case where they could possibly have "fixed" things for the caller. In this case it would have been safe to consider negative numbers as the same as 0 and null enumerables as the same as an empty one. They decided to throw anyway. Why?

Well, we have three cases to consider when writing constructors (and indeed other methods).

  1. Caller gives us values we can use directly.

Eh, use them.

  1. Caller gives us values we can't meaningfully use at all. (e.g. setting a value from an enum to an undefined value).

Definitely throw an exception.

  1. Caller gives us values we can massage into useful values. (e.g. limiting a number of results to a negative number, which we could consider as the same as zero).

This is the trickier case. We need to consider:

  1. Is the meaning unambiguous? If there's more than one way to consider what it "really" means, then throw an exception.

  2. Is the caller likely to have arrived at this result in a reasonable manner? If the value is just plain stupid, then the caller presumably made a mistake in passing it to the constructor (or method) and you aren't doing them any favours in hiding their mistakes. For one thing, they're quite likely making other mistakes in other calls but this is the case where it becomes obvious.

If in doubt, throw an exception. For one thing if you're in doubt about what you should do then it's likely the caller would be in doubt about what they should expect you to do. For another it's much better to come back later and turn code that throws into code that doesn't than turn code that doesn't throw into code that does, because the latter will be more likely to turn working uses into broken applications.

So far I've only looked at code that can be considered validation; we were asked to do something silly and we refused. Another case is when we were asked to do something reasonable (or silly, but we couldn't detect that) and we weren't able to do it. Consider:

new FileStream(@"D:\logFile.log", FileMode.Open);

There's nothing invalid in this call that should definitely fail. All validation checks should pass. It will hopefully open the file at D:\logFile.log in read mode and give us a FileStream object through which we can access it.

But what if there's no D:\logFile.log? Or no D:\ (amounts to the same thing, but the internal code might fail in a different way) or we don't have permission to open it. Or it's locked by another process?

In all of these cases we're failing to do what is asked. It's no good our returning an object that represents attempts to read a file that are all going to fail! So again, we throw an exception here.

Okay. Now consider the case of StreamReader() that takes a path. It works a bit like (adjusting to cut out some indirection for the sake of example):

public StreamReader(String path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize)
{
    if (path==null || encoding==null)
        throw new ArgumentNullException((path==null ? "path" : "encoding"));
    if (path.Length==0)
        throw new ArgumentException(Environment.GetResourceString("Argument_EmptyPath"));
    if (bufferSize <= 0)
        throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
    Contract.EndContractBlock();

    Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan, Path.GetFileName(path), false, false, true);
    Init(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, false);
}

Here we've got both cases where a throw might happen. First we've got validation against bogus arguments. After that we've a call to the FileStream constructor that in turn might throw an exception.

In this case the exception is just allowed to pass through.

Now the cases we need to consider are a tiny bit more complicated.

With most of the validation cases considered at the beginning of this answer, it didn't really matter in what order we did things. With a method or property we have to make sure that we've either changed things to be in a valid state or thrown an exception and left things alone, otherwise we can still end up with the object in an invalid state even if the exception was thrown (for the most part it suffices to do all of your validation before you change anything). With constructors it doesn't much matter what order things are done in, since we're not going to return an object in that case, so if we throw at all, we haven't put any junk into the application.

With the call to new FileStream() above though, there could be side-effects. It's important that it is only attempted after any other case that would throw an exception is done.

For the most part again, this is easy to do in practice. It's natural to put all of your validation checks first, and that's all you need to do 99% of the time. An important case though is if you are obtaining an unmanaged resource in the course of a constructor. If you throw an exception in such a constructor then it will mean the object wasn't constructed. As such it won't be finalised or disposed, and as such the unmanaged resource won't be released.

A few guidelines on avoiding this:

  1. Don't use unmanaged resources directly in the first place. If at all possible, work through managed classes that wrap them, so it's that object's problem.

  2. If you do have to use an unmanaged resource, don't do anything else.

If you need to have a class with both an unmanaged resource and other state, then combine the two guidelines above; create a wrapper class that only deals with the unmanaged resource and use that within your class.

  1. Better yet, use SafeHandle to hold the pointer to the unmanaged resource if at all possible. This deals with a lot of the work for point 2 very well.

Now. What about catching an exception?

We can certainly do so. The question is, what do we do when we've caught something? Remember that we have to either create an object that matches what we were asked for, or throw an exception. Most of the time if one of the things we've attempted in the course of that fails, there isn't anything we can do to successfully construct the object. We're likely therefore to just let the exception pass through, or to catch an exception just to throw a different one more suitable from the perspective of someone calling the constructor.

But certainly if we can meaningfully continue on after a catch then that's allowed.

So in all, the answer to "Can you use a throw or try and catch within a constructor?" is, "Yes".

There is one fly in the ointment. As seen above the great thing about throwing within a constructor is that any new either gets a valid object or else an exception is thrown; there's no in between, you either have that object or you don't.

A static constructor though, is a constructor for the class as a whole. If an instance constructor fails, you don't get an object but if a static constructor fails you don't get a class!

You're pretty much doomed in any future attempt to make use of that class, or any derived from it, for the rest of the life of the application (strictly speaking, for the rest of the life of the app domain). For the most part this means throwing an exception in a static class is a very bad idea. If it's possible that something attempted and failed might be attempted and succeed another time, then it shouldn't be done in a static constructor.

About the only time when you want to throw in a static constructor is when you want the application to totally fail. For example, it's useful to throw in a web application that lacks a vital configuration setting; sure, it's annoying to have every single request fail with the same error message, but that means you're sure to fix that problem!

Solution 2

Can you use a throw or try and catch within a constructor?

Both are possible.

If an exception can happen during construction of an object instance, and there is something you can do about it, catch it and try to fix it.

If there is nothing you can do about the exception, it is generally best to allow it to propagate (rather than leaving the object instance in an improperly initialized state).

Solution 3

The normal lifecycle of an object is interrupted if an exception is thrown in a instance constructor. So if the object has a destructor to clean up any resources then an exception thrown in the constructor will prevent the destructor running and thus forms a memory leak. Always catch, clean up, and re-throw if you have any disposable resources allocated in the constructors or for any fields.

If an exception is thrown in a static constructor then the class is no longer in a state that can be used by the AppDomain. So always catch exceptions in static constructors and deal with them. Do not re-throw and do not leave them uncaught.

Catching and handling exceptions in any constructor is fine to do.

But as per normal, it is 1000x better to code to avoid exceptions rather than let them occur.

See MSDN Constructor Design & Eric Lippert's Vexing Exceptions.

Share:
16,463
Admin
Author by

Admin

Updated on June 22, 2022

Comments

  • Admin
    Admin about 2 years

    Can you use a throw or try-catch within a constructor?
    If so, what is the purpose of having a constructor that has an argument that can throw an exception?

    This constructor is an example:

    public Chat()
    {
        chatClient = new Client(Configuration.LoginEmail, Configuration.LoginPassword);
        chatRoom = chatClient.JoinRoom(Configuration.RoomUrl);
    }
    

    The line chatRoom = chatClient.JoinRoom(Configuration.RoomUrl); can throw exceptions.