Goals:
- Avoid Rust's Option/Either manual error handling strategy.
- Make exceptions cheaper.
- Avoid error translation between Nim libraries by construction. The new vocabulary type
ErrorCode
is what should be used. Wrappers should translate errors toErrorCode
. - Make the error
out of memory
easier to handle. - Resolve once and for all the "error codes vs exceptions" choice.
The core of this proposal is a new enum called ErrorCode
that covers everybody's use case.
The different error states have been modelled to map reasonably clearly to POSIX errno values as
well as to HTTP error codes.
Furthermore the following documents have been a source of inspiration:
Software system | Link |
---|---|
Godot Game Engine | https://docs.godotengine.org/en/stable/classes/[email protected]#enum-globalscope-error |
SQLite | https://www.sqlite.org/rescode.html |
POSIX Errno values | https://pubs.opengroup.org/onlinepubs/9699919799.2016edition/basedefs/errno.h.html |
Maria DB | https://mariadb.com/kb/en/mariadb-error-code-reference/ |
OpenCL error codes | https://gist.github.com/bmount/4a7144ce801e5569a0b6 |
Mongo DB | https://www.mongodb.com/docs/manual/reference/error-codes/ |
Windows API | https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499- |
HTTP status codes | https://www.rfc-editor.org/rfc/rfc9110.html#name-status-codes |
Nim enum | Posix | HTTP | description |
---|---|---|---|
Success | 0'i32 | 200, 201, 202 | Operation completed successfully. |
OverflowError | EOVERFLOW | Integer overflow or underflow error. | |
Failure | 500 | General failure, unknown error, etc. | |
BugError | Programming bug detected. | ||
IndexError | Array index out of bounds. | ||
RangeError | ERANGE, EDOM | 416 | Range check error. |
OverlapError | Source and destination memory overlaps. | ||
SyntaxError | 422 | A general parsing error. | |
OutOfMemError | ENOMEM | Not enough memory left. | |
DiskFullError | ENOSPC | 507 | No space left on device. |
StackOverflow | No stack space left. | ||
IOError | EIO | I/O error. | |
ValueError | EINVAL, EBADMSG, EILSEQ, ENOMSG, EDESTADDRREQ | Invalid argument. Or: Bad/missing message. | |
KeyError | Invalid key. | ||
EndOfStreamError | End of stream/file reached. | ||
SkipError | Skip to next item. | ||
FullError | E2BIG, ENOBUFS | No space left in the buffer. Or: Argument list too long. | |
EmptyError | ENODATA | No message is available. | |
BusyError | EBUSY, ETXTBSY | 429 | Device is busy. Too many requests. |
DeadResource | ECHILD, EOWNERDEAD | Dead thread/owner/child. | |
ResourceExhaustedError | ENOLCK | Thread/process/etc creation failed. | |
DescriptorExhaustedError | ENFILE | Too many files open in system. | |
PermissionDenied | EACCES, EPERM | 403, 401, 407, 405, 406 | Permission denied. |
RetryError | EAGAIN, EWOULDBLOCK | Resource unavailable, try again. | |
TimeoutError | ETIMEDOUT, ETIME | 408, 504 | Connection timed out. |
InterruptedError | EINTR | Interrupted function. | |
DeadlockError | EDEADLK | 508 | Resource deadlock would occur. |
LockedError | Resource is locked. | ||
FormatMismatch | Source and destination have incompatible formats. | ||
AlreadyConnected | EADDRINUSE, EISCONN | Address in use. Socket is connected. | |
AddressNotAvailable | EADDRNOTAVAIL | Address not available. | |
AddressFamilyUnsupported | EAFNOSUPPORT | Address family not supported. | |
BadOperation | EOPNOTSUPP, ENOTSUP, ENOSYS, EPROTONOSUPPORT, ENOTTY, ESPIPE, EISDIR, ENOTEMPTY | 400, 415 | Operation not supported. Bad Request. |
AbortedOperation | ECANCELED, ECONNABORTED, ENETRESET | Operation canceled. Connection aborted. | |
UnimplementedOperation | 501 | Operation is not implemented. | |
AlreadyInProgress | EALREADY, EINPROGRESS | Operation already in progress. | |
NameTooLong | ENAMETOOLONG | 414, 431 | Path/Filename/URL too long. |
NameExists | EEXIST | Name file/directory already exists. | |
NameNotFound | ENOENT, EIDRM, ENODEV, ENOTDIR | 404 | No such file or directory or device. |
ContentTooLong | EFBIG, EMSGSIZE | 413 | File/content too large. |
BadDescriptor | EPIPE, EBADF, EMFILE, ENOSTR, ENOTSOCK, ENOSR, ENXIO, ESRCH | Bad file descriptor/pipe/process/etc. | |
BadExecutable | ENOEXEC | Executable file format error. | |
BadLink | ELOOP, EMLINK, EXDEV | 421 | Too many levels of symbolic links. Too many links. Cross-device link. HTTP: Misdirected request. |
BadProtocol | EPROTOTYPE, ENOPROTOOPT | 505 | Protocol wrong type for socket. Protocol not available. HTTP version not supported. |
ProtocolError | EPROTO | Protocol error. | |
ReadonlyProtection | EROFS | Cannot write to readonly data. | |
SegFault | EFAULT | Bad address. Segmentation fault. Nil pointer derefence. | |
DiskCorruption | Corrupted disk/file/table. | ||
Disconnected | ENETDOWN, ENETUNREACH, ECONNRESET, ENOTCONN | Network is down. Network unreachable. Connection reset. The socket is not connected. | |
RefusedConnection | ECONNREFUSED | Connection refused. | |
UnreachableHost | EHOSTUNREACH | 502 | Host is unreachable. Bad Gateway. |
UnrecoverableState | ENOTRECOVERABLE | State not recoverable. | |
AuthenticationRequired | 511 | Network authentication required. | |
RedirectError | 308, 307 | Redirect to other URL/path. | |
Reserved1 | Reserved for future extensions. This field will then be renamed! | ||
Reserved2 | Reserved for future extensions. This field will then be renamed! | ||
Reserved3 | Reserved for future extensions. This field will then be renamed! | ||
Reserved4 | Reserved for future extensions. This field will then be renamed! | ||
Reserved5 | Reserved for future extensions. This field will then be renamed! | ||
Reserved6 | Reserved for future extensions. This field will then be renamed! | ||
Reserved7 | Reserved for future extensions. This field will then be renamed! | ||
Reserved8 | Reserved for future extensions. This field will then be renamed! | ||
Reserved9 | Reserved for future extensions. This field will then be renamed! |
A routine can be annoted with {.raises: ErrorCode.}
if it raises such a cheap exception. Likewise except ErrorCode as e
exists. The raise
statement can be used to raise a cheap exception.
Routines that are not annotated with {.raises: ErrorCode.}
cannot use raise ErrorCode.x
nor can they call into
routines that are marked with {.raises: ErrorCode.}
unless of course such calls happen within an try except
environment.
I consider this to be the sweet spot in language design. Individual callsites are not annotated but routine headers have to be. The granularity feels just right for Nim.
There is a new switch called --panics:errorcode
.
Existing errors "raised" directly by the Nim runtime esp RangeDefect
, IndexDefect
and OverflowDefect
are mapped to the corresponding ErrorCode
by the compiler if a new mode --panics:errorcode
is enabled.
This means that an operation like a + b
that can overflow inside a routine annotated with {.raises: ErrorCode.}
.
The justification for this is that the routine clearly communicates that it is an operation that can fail. The caller
prepares for a failure already. Adding the "bug" reason to the failure mode has no downside in practice.
a + b
can produce an OverflowError
that is reported back to the caller as if a statement like raise OverflowError
would
have been written in the code directly:
proc canOverflow() {.raises: ErrorCode.} =
echo a + b
try:
canOverflow()
except ErrorCode as e:
assert e in {IOError, OverflowError}
With --panics:off
(the default mode) canOverflow
continues to produce an OverflowDefect
.
The same is true for "out of memory" errors:
proc canRaiseOOM() {.raises: ErrorCode.} =
discard alloc(HugeVal)
proc canPanicOnOOM() =
discard alloc(HugeVal)
try:
canRaiseOOM()
except ErrorCode as e:
case e
of OutOfMemError:
echo "Could gracefully recover from OOM!"
try:
canPanicOnOOM()
except:
echo "Damn, it quit()ed instead."
Translating between a carry flag and ErrorCode
is cheap and obvious:
For example:
# NEW BUILTINs offered by system.nim
proc checkedAdd(a, b: int): (int, int) {.raises: [].}
proc checkedMul(a, b: int): (int, int) {.raises: [].}
proc `*`(a, b: int): int {.raises: ErrorCode.} =
# Possible implementation of builtin `*`.
let (lo, hi) = checkedMul(a, b)
if hi != 0:
raise OverflowError
else:
return lo
proc p(args) {.raises: ErrorCode.}
is translated to proc p(args): ErrorCode
.
proc p(args): T {.raises: ErrorCode.}
is translated to proc p(args): (ErrorCode, T)
when T
is an integral type (int etc.).
It is translated to proc p(result: out T; args): ErrorCode
otherwise. This seems to produce the best code for the common
architectures (x86, x86_64, ARM, RISC V). The errors are propagated through CPU registers.
proc fib(n: int): int {.raises: ErrorCode.} =
if n < 0:
raise RangeError
if n < 2:
n
else:
fib(n-1) + fib(n-2)
try:
let x = fib()
except ErrorCode as e:
echo "Problem: ", e