March 24, 2017
On Friday, 24 March 2017 at 00:28:16 UTC, Walter Bright wrote:
> The string is what gets printed to the user like:
>
>    "your password has to have at least one upper case character in it"
>
> In general, such is not deducible from the type/arguments.

Yes, of course they are. In the simplest case in D, we could do throw new ExceptionImpl!string instead of throw new Exception(string), then it is absolutely deducible from the type, since it is a part of the type by definition.

That also becomes catchable independently of other exceptions (have you ever done something like `catch(Exception e) if(e.msg != "foo") throw e;`? I actually have literally done that with a crappy D library that threw an exception that I could recover from with a retry...), avoids any runtime allocation of the string, and is potentially easier to internationalize if you want to present it to the end user.

That's not how I'd literally do it, but it is something we CAN do in D (and would be an improvement from the status quo, without being a hassle of hierarchies).

> Exceptions are not for debugging the code (that's what Errors are for), Exceptions are for informing the user that he did it wrong.

All the more reason to actually make them as informative as possible (...and likely to cut out most the irrelevant stack trace). If the user is another programmer calling your function, they ought to be able to catch and inspect the exception in order to retry.

Moreover, informing the user wants to have as much info as possible. We've probably all been frustrated by in D by "Range violation" but it doesn't tell us what the value actually was. Attaching an int and sinking it into the error message ought to be trivial... and it IS, yet druntime doesn't do it. (and yes, I know that's an Error, not an Exception, but same reasoning applies)

I only care about the hierarchy insomuch as I intend to catch it without catching something else from the same block of code.


try {
    throw new RangeErrorImpl!(size_t, size_t, size_t)(x, 0, array.length);
} catch(RangeError e) {
    // this suddenly became a billion times more useful
    e.toString(sink); // RangeError, 10, 0, 8
}
March 24, 2017
On Thursday, 23 March 2017 at 20:48:24 UTC, Jonathan M Davis wrote:
> And in cases like D scripts, it would be overkill to be forced to create your own exception types, so it would really annoying if you couldn't create Exceptions.

Creating a new exception class is actually trivial  in D, we create new types all the time without even thinking about it almost every time we hit `!`, and could be even more so with some library helpers.

Catching the type when it doesn't have an outside name isn't as easy, but the randomly created types can still inherit from some base class that is caught, but the templated/generated implementation overrides various interface functions to provide more detailed, rich information (and, of course, you CAN still catch it specifically if you really wanted to).
March 24, 2017
On Fri, Mar 24, 2017 at 01:44:02AM +0000, Adam D. Ruppe via Digitalmars-d wrote:
> On Friday, 24 March 2017 at 00:28:16 UTC, Walter Bright wrote:
> > The string is what gets printed to the user like:
> > 
> >    "your password has to have at least one upper case character in
> >    it"
> > 
> > In general, such is not deducible from the type/arguments.
> 
> Yes, of course they are. In the simplest case in D, we could do throw new ExceptionImpl!string instead of throw new Exception(string), then it is absolutely deducible from the type, since it is a part of the type by definition.
> 
> That also becomes catchable independently of other exceptions (have you ever done something like `catch(Exception e) if(e.msg != "foo") throw e;`? I actually have literally done that with a crappy D library that threw an exception that I could recover from with a retry...), avoids any runtime allocation of the string, and is potentially easier to internationalize if you want to present it to the end user.

Catching an Exception by message? That sounds like horrible code smell to me. Upgrade the 3rd party library (which may change the message) and suddenly your program breaks. Bad idea.

It probably makes sense for 3rd party libraries to have at least a subclass of Exception, so that you can catch errors originating from that library rather than everything in general.  Beyond that, though, you're treading into implementation details territory, which is IMO a bad idea in terms of breaking encapsulation, overly tight coupling between thrower/catcher, etc..

A better way, though, is for the library to provide a retry mechanism in the first place, as part of the official API, than to rely on the caller catching an exception and identifying it.


> That's not how I'd literally do it, but it is something we CAN do in D (and would be an improvement from the status quo, without being a hassle of hierarchies).

Generally, I find that I do subclass Exception in my own code, for obvious reasons, including that since it's my own code, the rest of the code knows what to catch and what to do about it.  It basically becomes an API common to code in that particular project that defines certain exception subclasses with known semantics.

In 3rd party libraries, I can see a handful of exception subclasses publicly documented for that library as part of its API as stuff specific to that library that may get thrown.

But I don't see how to generalize that across libraries... by that point, it will have to become so generic that it's no better than just throwing Exception with a string message.


> > Exceptions are not for debugging the code (that's what Errors are for), Exceptions are for informing the user that he did it wrong.
> 
> All the more reason to actually make them as informative as possible (...and likely to cut out most the irrelevant stack trace). If the user is another programmer calling your function, they ought to be able to catch and inspect the exception in order to retry.
> 
> Moreover, informing the user wants to have as much info as possible. We've probably all been frustrated by in D by "Range violation" but it doesn't tell us what the value actually was. Attaching an int and sinking it into the error message ought to be trivial... and it IS, yet druntime doesn't do it. (and yes, I know that's an Error, not an Exception, but same reasoning applies)

Actually, I see this as evidence *against* having RangeError in the first place.  If we had stuck to throwing Exception or, in this case, Error, that would have prompted whoever wrote the bounds check code to actually write a useful error message instead of just throwing RangeError with zero additional details.  (Since, after all, the exception type ought to be enough to tell the user what needs to be known.)


> I only care about the hierarchy insomuch as I intend to catch it without catching something else from the same block of code.
> 
> 
> try {
>     throw new RangeErrorImpl!(size_t, size_t, size_t)(x, 0, array.length);
> } catch(RangeError e) {
>     // this suddenly became a billion times more useful
>     e.toString(sink); // RangeError, 10, 0, 8
> }

Except that it's not really *that* useful if you don't know what those numbers mean. They could be values taken from CPU registers in a stacktrace for all I know, which tells me nothing.  Meaning that you might as well have formatted those numbers into part of the string message in the first place.

A similar discussion came up some years ago, where Andrei proposed adding a Variant[string] field to Exception where the thrower can stick whatever details he likes into it.  Again, though, the problem is, how would the catcher know what string keys to use to get useful info out of that field, and what the values might mean.  Unless there is some documented standard, it's really not of much use.  It's like receiving an XML file with no DTD. Yeah I'm sure the meaning is all there encoded in the structure somehow, but it does me no good if I don't know what it all means.

Besides, even if we had Variant[string] in Exception, would people really bother to write the additional code to populate it with meaningful data? (Much less read a spec on standard key/value mappings.)

Usually when I write a `throw Exception`, my mind is thinking, "yeah this is a case the code can't handle, and the user shouldn't pass to us, so just throw an Exception, get it out of the way, and let me get on to the real algorithm already."  I wouldn't have the motivation to want to manually populate a hash with details that the user code might not even care about in the end.  It's already kind enough on my part to actually want to use std.format to produce a message that a human reader might find helpful, like "syntax error on line 36: unexpected character 'x'". I could have just done a `throw new Exception("syntax error");` and be done with it.  At least I, the human, would read that message if the Exception got thrown, but put in additional effort so that it's parseable by code? No thanks.


T

-- 
Lottery: tax on the stupid. -- Slashdotter
March 25, 2017
On Friday, 24 March 2017 at 19:38:14 UTC, H. S. Teoh wrote:
> Catching an Exception by message? That sounds like horrible code smell to me.

Yes, it is. That's why I think exception strings are an antipattern. You should be making new classes, not new strings.

But, D lets us have both worlds. Consider the following:

---

class MallocedException : Exception {
	@nogc
	this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
		super(msg, file, line, next);
	}

	@nogc
	private void freeSelf() {
		import core.stdc.stdlib;
		import core.stdc.stdio;
		printf("freeing\n");
		free(cast(void*) this);
	}

	~this() {
		freeSelf();
	}
}

class RaisedException(string msg) : MallocedException {
	@nogc
	this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
		super(msg, file, line, next);
	}
}

class RaisedExceptionDetails(string message, T...) : RaisedException!message {
	T args;
	@nogc
	this(T args, string file = __FILE__, size_t line = __LINE__, Throwable next = null) {
		this.args = args;
		super(file, line, next);
	}

	override void toString(scope void delegate(in char[]) sink) const {
		import core.internal.traits : externDFunc;
		alias sizeToTempString = externDFunc!("core.internal.string.unsignedToTempString",
						      char[] function(ulong, char[], uint) @safe pure nothrow @nogc);

		char[20] tmpBuff = void;

		sink("RaisedException");
		sink("@"); sink(file);
		sink("("); sink(sizeToTempString(line, tmpBuff, 10)); sink(")");

		if (message.length)
		{
		    sink(": "); sink(message);
		}

		foreach(idx, arg; args) {
			sink("\n\t[");
			sink(sizeToTempString(idx, tmpBuff, 10));
			sink("] = ");
			static if(is(typeof(arg) : const(char)[]))
				sink(arg);
			else static if(is(typeof(arg) : long))
				sink(sizeToTempString(arg, tmpBuff, 10));
			else
				{} // FIXME support more
		}

		if (info)
		{
		    try
		    {
			sink("\n----------------");
			foreach (t; info)
			{
			    sink("\n"); sink(t);
			}
		    }
		    catch (Throwable)
		    {
			// ignore more errors
		    }
		}


	}

}

@trusted @nogc
void raise(string msg, string file = __FILE__, size_t line = __LINE__, T...)(T args) {
	import core.stdc.stdlib, std.conv;
	enum size = __traits(classInstanceSize, RaisedExceptionDetails!(msg, T));
	void* buffer = malloc(size);
	assert(buffer !is null);
	throw emplace!(RaisedExceptionDetails!(msg, T))(buffer[0 .. size], args, file, line);
}

void main() {
	raise!"my_exception"(12, "additional info", 32);
}

---


That raise line lets you define your string if you must, as well as *any* other data you want to pass along, with a usable, if a bit less than ideal, toString representation of it.

BTW, I also went ahead and made this @nogc to show that's not really so hard to do. If you catch a MallocException, just .destroy(it). Easy (at least if you control both sides of the code).

Notice that there are also no Phobos imports and no user allocations at the raise site.



The only thing I'd add to it is some kind of raise!YourExceptionBase overload and I'd find this pretty useful.


> It probably makes sense for 3rd party libraries to have at least a subclass of Exception, so that you can catch errors originating from that library rather than everything in general.

I tend to do very broad classifications. My script.d, for example, can throw three supertypes of exception: compile exception, runtime exception, and user-defined exceptions out of the script.

Those are the things you'd likely catch, because each one has a different yet reasonable possible response.

But, if additional info is easy to attach, I'd go ahead and do that too - there's just no reason not to when it is easy, and the catcher can then decide to get into more details if interested, or just catch one of the super classes if not.

> Actually, I see this as evidence *against* having RangeError in the first place.  If we had stuck to throwing Exception or, in this case, Error, that would have prompted whoever wrote the bounds check code to actually write a useful error message

Come on, be realistic. We both know it would be `throw new Exception("RangeError");` with no additional data.

In fact, since building strings is *significantly* more difficult than just passing arguments, building a useful string is expected to be uncommon... and it is. Grep phobos and find the majority of exception strings are static literals, even when more information could be available.

> Except that it's not really *that* useful if you don't know what those numbers mean.

The class toString could also label them trivially, or, of course, you could document it for those who care (and those who don't care will just learn to ignore the noise).

It is far more likely that the class toString would give useful results than throw new Exception(format(...)) since:

1) it is written once, at the class definition (or in the mixin reflection template), instead of at every throw point

and 2) functions like format("") aren't available in a LOT of contexts, whereas toString(sink) from data internal to the exception can be done virtually anywhere. format("") adds a Phobos dependency, making it a no go for druntime, it adds a GC dependency, making it no go in those nogc contexts, and having to add the import can be as much of a pain as importing a exception helper template anyway.

But even if it isn't labeled, you could look it up, or get used to it, or maybe even guess it from context. The point is that the info is THERE for those who want to read it. (Frankly, I prefer just segfaulting than throwing the useless RangeError as it is now, at least a core dump can be loaded up in a debugger.)

> A similar discussion came up some years ago, where Andrei proposed adding a Variant[string] field to Exception where the thrower can stick whatever details he likes into it.

Yeah, that's useless, it has all the same problems as format(), plus some.

> It's already kind enough on my part to actually want to use std.format to produce a message that a human reader might find helpful, like "syntax error on line 36: unexpected character 'x'". I could have just done a `throw new Exception("syntax error");` and be done with it.  At least I, the human, would read that message if the Exception got thrown, but put in additional effort so that it's parseable by code?

Reminder: we're using D here. It is LESS effort to add info than it is to call format().
March 25, 2017
On 2017-03-23 21:48, Jonathan M Davis via Digitalmars-d wrote:

> There are plenty of cases where all you care about is that something went
> wrong when calling a function and aren't going to do anything special
> depending on what went wrong. You just handle it and move on.

If you don't know that went wrong you cannot handle it. Basically the only option that is left is to abort the program.

-- 
/Jacob Carlborg
March 25, 2017
On 2017-03-24 01:25, Walter Bright wrote:

> It's a compelling point, and is the reason why there are many different
> exception types in Phobos.
>
> But in my experience, if an error happens and it is local enough to be
> dealt with specifically, it is checked for directly without needing to
> go through an exception.

The problem is when something in the environment happens that you have no control over. Say you're program is supposed to read a file. You naturally first check if the file exists, then start reading it. What if the file exists on a USB memory which are unplugged in the middle of reading the file. If the program knows that the file suddenly doesn't exist anymore, then it can ask the user for another file, or plug the USB memory back.

> If one is far enough removed from the source of
> the error that one is relying on exceptions, pretty much all one can do
> is the binary "it worked" or "it did not work". The "why it did not
> work" message is then logged or sent to the user, that is the only
> recovery necessary.
>
> The fine grained Exception types is one of those things that sounds
> compelling but just doesn't seem to pay off in practice.

I don't agree. If think you have quite a narrow look at this. Example:

1. An application is internationalized. Showing a generic message in English would be pretty bad user experience. Translating the whole message directly would be a bad idea, I consider the message in an exception from a library an implementation detail. The best would be, in my opinion, to have enough information available in the exception type to build a proper translated error message for the user.

2. Another example is how the editor I use, TextMate, behaves. If I try to save a file I don't have permission to write to, it will ask for my password, basically doing the same thing as "sudo" would.

3. If I've opened a file in TextMate, then remove the directory the file is located in and then save the file in TextMate, it will show a message with the option to create the folder again.

For the last two examples, if the application only got a generic Exception or FileException it would not be possible implement these features because it would know if the error is that the file doesn't exist anymore or if I don't have permission to write to the file.

Talk about just providing a generic exception type. Take this error message, using std.file.copy:

std.file.FileException@d.d(3354): /tmp/bar/foo.txt: Permission denied

It doesn't tell if the failed permission is for the source or for the target. If failed to read or failed to write.

-- 
/Jacob Carlborg
March 25, 2017
On 2017-03-23 21:45, Adam D. Ruppe wrote:

> I'm of the firm belief that exceptions should NEVER have a string
> message - that's a code smell. If you do it right, all the info can be
> determined automatically by the exception type and passed arguments and
> *that* is what gets printed to string.

Exactly.

-- 
/Jacob Carlborg
March 25, 2017
On 2017-03-24 20:38, H. S. Teoh via Digitalmars-d wrote:

> It's already kind enough on my part to actually
> want to use std.format to produce a message that a human reader might
> find helpful, like "syntax error on line 36: unexpected character 'x'".
> I could have just done a `throw new Exception("syntax error");` and be
> done with it.  At least I, the human, would read that message if the
> Exception got thrown, but put in additional effort so that it's
> parseable by code? No thanks.

If it's a specific exception type with a specific field for the line number and a field for a filename (if applicable) then it can be used in a text editor to navigate to that file and highlight the line.

-- 
/Jacob Carlborg
March 25, 2017
On 3/25/17 3:44 PM, Jacob Carlborg wrote:
> Take this error message, using std.file.copy:
>
> std.file.FileException@d.d(3354): /tmp/bar/foo.txt: Permission denied
>
> It doesn't tell if the failed permission is for the source or for the
> target. If failed to read or failed to write.

Would be great if you submitted a PR to improve this message. -- Andrei
March 25, 2017
On Saturday, March 25, 2017 14:17:10 Jacob Carlborg via Digitalmars-d wrote:
> On 2017-03-23 21:48, Jonathan M Davis via Digitalmars-d wrote:
> > There are plenty of cases where all you care about is that something
> > went
> > wrong when calling a function and aren't going to do anything special
> > depending on what went wrong. You just handle it and move on.
>
> If you don't know that went wrong you cannot handle it. Basically the only option that is left is to abort the program.

In my experience, that's often not true. It could simply mean that you abort the current operation - especially in programs that act anything like servers or operate on batches of jobs. Yes, in some cases, you simply can't do anything useful if the operation failed, and you do have to abort the program, but that's not always true. It very much depends on what the program is doing.

Now, if you want to respond in any manner beyond "something went wrong," you're likely going to need a more specific exception type that tells you something about what actually went wrong, but I've seen plenty of code that really didn't care what went wrong - e.g. it simply logs the error and moves on to the next operation, or it responds to the program that made the request that something went wrong and then moves on to the next request.

- Jonathan M Davis