Usually this would be a forum post but I need decent table rendering so...
https://gist.github.com/Araq/3323cd15c8ccce1f42853bd586f24ebe
I suppose additional error information have to be given as return values?
proc p(args): T {.raises: ErrorCode.} is translated to proc p(args): (ErrorCode, T) when T is an integral type (int etc.). [...] This seems to produce the best code for the common architectures (x86, x86_64, ARM, RISC V). The errors are propagated through CPU registers.
For Windows x64 at most 64-bit structs may be returned via registers (which is easily hit as 64-bit integers are default in Nim). See https://godbolt.org/z/vKGoGxGrn
The same gose for 32-bit ARM except that it is only 32-bit. See https://godbolt.org/z/96W1oEvMK
For both cases there are exceptions. E.g. on 32-bit ARM a 64-bit integer or vector may be returned in two registers https://godbolt.org/z/MqMWeYr47 and for Windows x64 a 128-bit vector may be returned in an SSE register.
In the example about the red button, I was talking about some external factor that most of the code (especially other people's code) doesn't know about and enum ErrorCode. For example, vulkan has a VkResult https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkResult.html for example VK_ERROR_TOO_MANY_OBJECTS, it has ErrorCode FullError, but the party catching the exception can catch FullError, and usually it is memory specific on the process, but in vulkan this memory is on the gpu (and will probably have to be freed on the gpu). A regular Full Error does not take this specificity into account, so it would be nice to register your own GpuFullError (by the way, vulkan api has a lot of errors that don't really fit into the regular ErrorCode). Besides the same GpuFullError is useful because of raises pragma, for example I want to forbid the possibility to use functions that can overflow the number of objects on gpu.
As for adding Custom Error Codes via raises, I think it should be. However, I think that wherever there will be Error Codes documentation it should be mentioned that custom codes should be used only if it is really necessary (which should be very rare). Since otherwise it will get in the way.
The same goes for 32-bit ARM except that it is only 32-bit. See https://godbolt.org/z/96W1oEvMK
Strange, this doesn't match my experiments. I didn't keep the code though. :-/
for example VK_ERROR_TOO_MANY_OBJECTS
That would be ResourceExhaustedError, IMO btw.
Sometimes you need additional context on the error, for example when erroring out because a spawned process fails, the error code or kill signal could be useful.
Also, when handling the error, it's often useful to know what the program was doing. For example in an IOError, knowing the file path that caused the error can be very useful.
I was long thinking this kind of information is really mapping the stack frames. It's kind of stack frame context metadata for the errors. Could it be possible to have thread-local or stack-local variables where such context could be added in case of an error. That would make perfectly sense to me. Like a thread-local errno where it's possible to add string data.
And if it is a return-value, is there still room for additonal info?
Sure but this additional information is available via thread local storage. There should be some syntactic sugar for that but it's not yet written down.
Yes canOverflow can produce NameNotFound in theory.
It means, when using library function proc someFn() {.raises: ErrorCode.} - there's no way to understand what are the special cases (errors) for this proc and how they should be handled. Basically it's as good as using int instead of ErrorCode - basically, loosing the type safety.
Basically, it's same as using huge FlagCode enum for everything proc openFile(fname: string, flag: FlagCode) and proc startHttpServer(port: int, flag: FlagCode).
Maybe it's better to be clear about what it is - a way to achieve high performance, with the cost of loosing type safety and readability. And use plain int instead of ErrorCode.
Explicit error return with the result, works very well in this case, as it forces you to explicitly handle all possible scenarios. Using ErrorCode will be very hard to use.
Say you use sendData proc. from "networking library v1", you carefully wrote your code, and handled all the error cases. But after the update to "networking library v2", the sendData implementation has been changed, and now it throws one more type of exception. With specific error types the compiler would fail and notify you, with ErrorCode you would have no idea that the code is not valid anymore and the server may crash because of new unhandled error type.
I would like add my two cents on this topic from software engineering viewpoint.
Before comment on the proposal, I want to state some opinionated principles about error handling.
These principles applies to multi-language multi-serivce system. For example, in a micro-services environment, a service A written in Nim make an HTTP call to service B written in Java, and service B encounter an exception that it is not responsible to, then service B should return a json response (or whatever format) that contain all necessary info (e.g. error code, error message...) that service A can reconstruct the error completely in Nim. And if serivce A also not responsible to that exception, this process will continue.
Go back to the proposal, some comments:
It is not a proposal. I know it says it is one but it's in a "pre" state. It's not an RFC. ;-)
As for the other arguments, typically it boils down to "I need more control". Sure, fair enough, go ahead. By using the function's return type, not the exception mechanism which is by design uses a different granularity.
Why not something like a function out parameter or return value of type ErrorCode that the caller has the option to either ignore, in which case the error would be propagated automatically, or to use and act on it, in which case the error is handled by the caller and not propagated.
This different than the try ... except syntax because try would catch any exception thrown by the code within whereas the out parameter could propagate only some errors the inner function chose to make available while still keeping the ability to throw unrelated exceptions.
proc perform_some_request(args: Args, err: var ErrorCode) =
let req = newRequest(args) # allocation could fail and will throw
req.perform(err) # request could fail but the error is nicely propagated to the parent
try:
var err ErrorCode
perform_some_request(args, err)
if err:
# the request could not complete, do something reasonable for it
except:
# there is an unrecoverable error, log it and exit
If the error argument is not provided, then the exception is thrown. Of course if the ErrorCode is just an int, the message and stack trace and context would be accessible via local storage transparently.