The application this question is about is basically a transpiler which contains a lot of logic. The transpiler is written in C++ (which should not be much of relevance for this question), and it transpiles a special language (which we developed) into a different language.
For error handling (errors originating in invalid input as well as internal errors of the transpiler), I use exceptions. To keep the transpiler's logic as simple as possible, the input is validated very late, i.e. in most cases no separate validation is taking place. This means, the processing logic throws exceptions. Some utility functions are used globally thoughout the code base, and those can throw exceptions, too.
That means, exceptions are potentially thrown in a utility function, in which we do not know where the input comes from. That is a problem, because -- similar to how your compiler tells you in which line of which file the parse error occurred -- I'd like to tell the user of the transpiler where the error originated. To keep things simple, that does not necessarily have to be some "stack" or "chain" of contexts. But the exceptions should carry at least some contextual information, which part of the input is invalid and why, such that the relevant part of the input can be corrected and/or an IDE can navigate to the error's source.
For the exceptions, I currently use one single class Error
which carries all information as members, and static functions are used to create them depending on what error occurred, for example:
throw Error::internalError("This should not have happened...");
or a more interesting case (pseudo code, this is not C++):
var x = ...;
if (!x.isInteger())
throw Error::notAnInteger(x);
Now I'd like to augment that exception (the Error
object) with a "context / location specification". For example, in my transpiled language, I have the concept of variables, which have some predefined value. Let's consider a variable which has an internal ID stored as an integer, and a value which we can retrieve. Now we want to make sure that the value is an integer (again, pseudo code):
var variableID = ...;
var value = getValueOfVariable(variableID);
if (!value.isInteger())
throw Error::notAnInteger(value);
Now consider the case where variableID = 42, value = "foo"
. When the exception will be thrown and presented to the user, she only knows that "foo"
is not an integer, but she is not being told that this value comes from the variable 42
.
That's why I want to tell the Error
somehow that we are trying to parse the variable whose ID is variableID
.
I'd like to present you three ideas which I came up with, but can't decide which one is better style.
The first idea is to add additional static functions, each for a type of such a context. In the example above, instead of
throw Error::notAnInteger(value);
I then do
throw Error::variableNotAnInteger(value, variableID);
and within that function I store the information that the error source was "variable with ID variableID
". However, with more and more such types of contexts (it is not only parsing the value of a variable which can lead to "not an integer" errors), the number of functions grows rapidly.
A second idea was to introduce a Location
class, which has the purpose of describing the error source / context, and pass that along with the other parameters to these static functions:
throw Error::notAnInteger(value, Location::variable(variableID));
However, that means that all utility functions (for example a function which has only the purpose to parse a value, independently of where the value comes from), would need to be augmented with the contextual Location
object, in order to
appropriately construct the Error
object to be thrown. That however makes the code base kind of ugly, in particular for nested and further nested logic.
A third idea is to add the location after the exception was thrown. The utility function throws an exception, and the caller then catches the exception, adds the appropriate location, and then throws the augmented exception again. This has the drawback that when I forget somewhere in the code base to add such a location, the error does not carry one. (In contrast, the second idea makes sure that a location object is being passed.)
This would look like that (again, pseudo code):
function parseValue(value, type) {
...
if (type == "integer") {
if (!value.isInteger()) {
// Throw an exception without a location / context information:
throw Error::notAnInteger(value);
}
return value.toInteger();
}
...
}
//-----------------------------------------
var variableID = ...;
var value = getValueOfVariable(variableID);
var type = getTypeOfVariable(variableID);
try {
var parsedValue = parseValue(value, type);
} catch (Error e) {
// Add the location of the error, and rethrow:
e.setLocation(Location::variable(variableID));
throw e;
}
Which of the above methods can be considered a good style? Is there even a fourth option I should consider? How is this typically done in trans-/compilers?