2018-10-29

error handling

about dealing with unexpected, or rare, return states of subroutines

usually subroutines have a main purpose, for example dividing two numbers or writing to a file, but sometimes this cant be fulfilled because for example:

  • external resources are unavailable, like inaccessible filesystem paths or error responses from webservices
  • the arguments are not supported: division by zero, invalid data types, missing data

states likes this are then often considered to be errors

how to communicate errors

types designated for errors

for example

  • boolean false
  • strings that are always considered error descriptions
  • null value types

values designated as errors

for example

  • negative or positive number on error
  • true on success boolean false on error

pro

  • type checks are usually fast and subroutines often return a limited number of different types con
  • problematic when all types can be valid results

error type

for example

  • a distinct type with fields for error identification, error group, description and possibly more

default values

for example

  • zero on failed parsing of a string to number

con

  • potentially ambiguous results (parsed zero string or failed parsing?)

pro

  • result is usually a valid return type to proceed and not corrupt following computation

additional return value

for example

  • extra output arguments that are set on error
  • return value as error status and output arguments for other values
  • additional error describing values returned on error

the additional values still have to be unpacked syntactically, which needs additional forms to create and accept multiple return values. the position of the error value in the collection of output values has to be agreed on between different libraries for consistency. similar to returning a single compound value or special error type. for every subroutine call and return, code has to be added to be check for an error status and it becomes a kind of manual flow control, with code that makes a call and checks if it shall continue. this interrupts subroutine composition

with output arguments

the error status can be passed as the subroutine result or output argument (or additional value). compare the following two signatures

input-value ... output-value ... output-status -> value
input-value ... output-value ... -> status

the latter should be simpler because there is always a status value and only one, and it isnt mixed with other output-values

local jumps

not exiting the subroutine but continuing at a place at the end of the subroutine where errors are handled. cleanup like deallocation before exit can be done at this place. a local variable can be used to save error details where the error occurs. local jumps can also exit loops

pro

  • fast
  • easily predictable
  • less duplication compared to handling errors at each source as all errors can be handled at one place

non-local jumps

error information can be collected and subroutines exited in the call stack upwards using non-local jumps until a catcher is met that handles the error. caller/callee return semantics are disregarded for the calls after the catcher. this corresponds to exception handling using throw and catch. with exception handling typically a backtrace is collected when an error occurs and a default catcher that exits the program is installed. exception handling can also be implemented with delimited continuations

cleanup (heap-memory, file handles or similar) might still be necessary on every non-local exit point. garbage collection helps with the memory management at least

pro

  • errors that might originate from multiple different subroutines can be handled at once
  • routine composition is not interrupted by status checks and can therefore be simpler in notation con
  • at some place, any errors might eventually still need to be handled
  • adds a way how subroutines can exit. normally subroutines exit at the point of their call or the whole process is ended. hidden control flow paths might emerge
  • no help if something needs to happen in context at every error source

continuation selection

multiple subroutine references passed to the subroutine and one is called on success the other on error. at some point the original subroutine can still return

con

  • signature of error handling routine has to be agreed on between different libraries for consistency
  • subroutine nesting, especially with anonymous functions

global variable

a global variable that is set to specific values on error. the variable needs to be thread local for mult-threaded error handling. the value persists between routine calls needs to be reset. might be harder to optimise by the compiler since the value can be changed from many places

other considerations

if multiple libraries use integer values for error ranges then error values of different libraries might overlap and be ambiguous. for this case, it can become necessary to keep additional information with error objects, for example a string identifier for the library it belongs to

reporting

error reporting is collecting details that may be helpful to fix the cause and making it available to the user. for example a message written to standard output or a log file

error details

information that can be useful when handling an error:

error-name
error-description
source-routine-name
source-line-number
source-file
source-module-name
other-contextual-data
backtrace
literal-irritant-expression

examples

glibc

most library functions return a special value to indicate that they have failed. the special value is typically -1, a null pointer, or a constant such as eof that is defined for that purpose. but this return value tells you only that an error has occurred. to find out what kind of error it was, you need to look at the error code stored in the variable errno

sph-sc-lib status

uses a struct for error integer identifier and string group. has helpers, that subroutine results can be passed to, that check the return value and go to the error label on error status. sph-sc-lib

int main() {
  status_declare;
  // code ...
  status_require(test());
  // more code ...
exit:
  return status.id;
}