How to use exceptions

Christian Köstlin christian.koestlin at gmail.com
Thu Aug 11 22:32:34 UTC 2022


Dear d-lang experts,

lets say in general I am quite happy with exceptions.
Recently though I stumbled upon two problems with them:
1. Its quite simple to loose valuable information
2. Its hard to present the exception messages to end users of your
    program

Let me elaborate on those:
Lets take a simple config parser example (pseudocode, the real code is 
in the end):
```d
auto parseConfig(string filename)
{
     return s
         .readText
         .parseJSON;
}
..
void main()
{
     ...
     auto config = parseConfig("config.json");
     run(config);
     ...
}
```
Lets look at what we would get in terms of error messages for the user 
of this program (besides that a full stacktrace is printed, which is 
nice for the developer of the program, but perhaps not so nice for the 
end user)
- file is not there: config.json: No such file or directory
   nice ... its almost human readable and one could guess that something
   with the config file is amiss
- file is not readable for the user: config.json: Permission denied
   nice ... quite a good explanation as well
- file with invalid UTF-8: Invalid UTF-8 sequence (at index 1)
   not so nice ... something is wrong with some UTF-8 in the program,
   but where and in which area of the program
- file with broken json: Illegal control character. (Line 4:0)
   not nice ... line of which file, illegal control character does
   not even sound like json processing.

Arguably readText could behave a little better and not throw away
information about the file its working with, but for parseJSON the
problem is "bigger" as it is only working with a string in memory not
with a file anymore, so it really needs some help of the application
developer I would say. When browsing through phobos I stumbled upon the
genius std.exception.ifThrown function, that allows for very nice
fallbacks in case of recoverable exceptions. Building on that I came up
with the idea (parhaps similar to Rusts error contexts) to use this
mechanism to wrap the exceptions in better explaining exceptions. This
would allow to provide exceptions with the information that might
otherwise be lost aswell as lift the error messages onto a end user 
consumable level (at least if only the topmost exception message is 
looked at).


```d
#!/usr/bin/env rdmd
import std;

auto contextWithString(T)(lazy scope T expression, string s)
{
     try
     {
         return expression();
     }
     catch (Exception e)
     {
         throw new Exception("%s\n%s".format(s, e.msg));
     }
     assert(false);
}

auto contextWithException(T)(lazy scope T expression, Exception 
delegate(Exception) handler)
{
     Exception newException;
     try
     {
         return expression();
     }
     catch (Exception e)
     {
         newException = handler(e);
     }
     throw newException;
}

// plain version, no special error handling
JSONValue readConfig1(string s)
{
     // dfmt off
     return s
         .readText
         .parseJSON;
     // dfmt.on
}

// wraps all exceptions with a description whats going on
JSONValue readConfig2(string s)
{
     // dfmt off
     return s
         .readText
         .parseJSON
         .contextWithString("Cannot process config file %s".format(s));
     // dfmt on
}

// tries to deduplicate the filename from the exception messages
// but misses on utf8 errors
JSONValue readConfig3(string s)
{
     // dfmt off
     auto t = s
         .readText;
     return t
         .parseJSON
         .contextWithString("Cannot process config file %s".format(s));
     // dfmt on
}

// same as 3 just different api
JSONValue readConfig4(string s)
{
     // dfmt off
     auto t = s
         .readText;
     return t
         .parseJSON
         .contextWithException((Exception e) {
             return new Exception("Cannot process config file%s\n 
%s".format(s, e.msg));
         });
     // dfmt on
}

void main()
{
     foreach (file; [
         "normal.txt",
         "missing.txt",
         "broken_json.txt",
         "not_readable.txt",
         "invalid_utf8.txt",
     ])
     {
 
writeln("=========================================================================");
         size_t idx = 0;
         foreach (kv; [
             tuple("readConfig1", &readConfig1),
	    tuple("readConfig2", &readConfig2),
	    tuple("readConfig3", &readConfig3),
             tuple("readConfig4", &readConfig4),
         ])
         {
             auto f = kv[1];
             try
             {
                 if (idx++ > 0) 
writeln("-------------------------------------------------------------------------");
                 writeln("Working on ", file, " with ", kv[0]);
                 f("testfiles/%s".format(file));
             }
             catch (Exception e)
             {
                 writeln(e.msg);
             }
         }
     }
}
```

What do you guys think about that?
Full dub project available at 
git at github.com:gizmomogwai/d-exceptions.git ... If you want to play with 
it, please run ./setup-files.sh first (which will create a file that is 
not readable).

One thing I am especially interested in would be how to annotate only
one part of a callchain with additional error information.
e.g. given the chain
```d
     someObject
          .method1
          .method2
          .method3
```
I would like to have the options to only add additional information to 
`method2` calls, without having to introduce intermediate variables (and
breaking the chain with it).

Also is there a way to do this kind of error handling without exceptions
(I think its hard to do this with optionals as they get verbose without
a special quick return feature).


Thanks in advance,
Christian




More information about the Digitalmars-d-learn mailing list