MathewN
MathewN

Reputation: 61

What are the best practices for returning one of mulitple errors from functions and ending the program in C?

I cann't find a way to return an error from a function inside an function, and ending the program.

I was thinking of using a struct and storing the returning value inside and end it this way, but I don't know if this is the best practise

Lets say I have a programm like this:

int main()
{
  // do stuff 
  importantFunction();
  // do stuff
  return 0;
}

In the importantFunction() I am calling two other functions which are doing some Bitshifting and are returning an array always. Now I want to try to return 1 (or 0x01 because the function is returning an array pointer) if an error happends in one of this function, but I am not sure how.

char *importantFunction()
{
  //do stuff
  secoundFunction();
  thirdFunction();
  //do stuff
  return array;
 }


char *secoundFunction()
{
  // do stuff
  if (something == x)
    return array;
  // do stuff
  return array;
}

I am just trying to find an methode without having to check the first function if it is equall to something and then end the program in int main. I am trying to avoid this, because it is not working always:

int main()
{
  // do stuff 
  char *pointer = importantFunction();
  if (*pointer == 'something')
    return 1;
  if (*pointer == 'something')
    return 2;
  if (*pointer == 'something')
    return 3;
  // and so on...
  // do stuff
  return 0;
}

I am sorry if this is a stupid question, I am not really good in asking questions.

Upvotes: 1

Views: 401

Answers (5)

Edwin Buck
Edwin Buck

Reputation: 70909

C has out-of-band error code handling, it's been in there since forever.

#include <errno.h>

int do_something(char* data) {
   if ( data == 0 ) {
      errno = ENODATA;
      return 0;
   }
   ... do stuff ...
}

... in the caller ...

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   return; // or exit;
} 

if wishing to chain errors

int value = do_something( "one" );
if ( int errornum = errno ) {
   fprintf("error (%d) could not do something: %s", strerror( errornum ) );
   errno = errornum;
   return; // or exit;
} 

Keep in mind that errno is reset by nearly every standard function call, so you need to capture it and then optionally set it again after you do whatever you want.

The reason errno is typically not used as heavily as it should be is probably becasue too many people are taught in-band error reporting (through special sentinels / values) first. In addition, it takes more lines of code to properly check error codes. That said, it is a far better solution, as you don't overload a return value with data and control information in the same variable.

There are already many error codes set, odd are you can reuse one for your needs, or pick one that's close enough

1  EPERM Operation not permitted
2   ENOENT  No such file or directory
3   ESRCH   No such process
4   EINTR   Interrupted system call
5   EIO     I/O error
6   ENXIO   No such device or address
7   E2BIG   Argument list too long
8   ENOEXEC     Exec format error
9   EBADF   Bad file number
10  ECHILD  No child processes
11  EAGAIN  Try again
12  ENOMEM  Out of memory
13  EACCES  Permission denied
14  EFAULT  Bad address
15  ENOTBLK     Block device required
16  EBUSY   Device or resource busy
17  EEXIST  File exists
18  EXDEV   Cross-device link
19  ENODEV  No such device
20  ENOTDIR     Not a directory
21  EISDIR  Is a directory
22  EINVAL  Invalid argument
23  ENFILE  File table overflow
24  EMFILE  Too many open files
25  ENOTTY  Not a typewriter
26  ETXTBSY     Text file busy
27  EFBIG   File too large
28  ENOSPC  No space left on device
29  ESPIPE  Illegal seek
30  EROFS   Read-only file system
31  EMLINK  Too many links
32  EPIPE   Broken pipe
33  EDOM    Math argument out of domain of func
34  ERANGE  Math result not representable
35  EDEADLK     Resource deadlock would occur
36  ENAMETOOLONG    File name too long
37  ENOLCK  No record locks available
38  ENOSYS  Function not implemented
39  ENOTEMPTY   Directory not empty
40  ELOOP   Too many symbolic links encountered
42  ENOMSG  No message of desired type
43  EIDRM   Identifier removed
44  ECHRNG  Channel number out of range
45  EL2NSYNC    Level 2 not synchronized
46  EL3HLT  Level 3 halted
47  EL3RST  Level 3 reset
48  ELNRNG  Link number out of range
49  EUNATCH     Protocol driver not attached
50  ENOCSI  No CSI structure available
51  EL2HLT  Level 2 halted
52  EBADE   Invalid exchange
53  EBADR   Invalid request descriptor
54  EXFULL  Exchange full
55  ENOANO  No anode
56  EBADRQC     Invalid request code
57  EBADSLT     Invalid slot
59  EBFONT  Bad font file format
60  ENOSTR  Device not a stream
61  ENODATA     No data available
62  ETIME   Timer expired
63  ENOSR   Out of streams resources
64  ENONET  Machine is not on the network
65  ENOPKG  Package not installed
66  EREMOTE     Object is remote
67  ENOLINK     Link has been severed
68  EADV    Advertise error
69  ESRMNT  Srmount error
70  ECOMM   Communication error on send
71  EPROTO  Protocol error
72  EMULTIHOP   Multihop attempted
73  EDOTDOT     RFS specific error
74  EBADMSG     Not a data message
75  EOVERFLOW   Value too large for defined data type
76  ENOTUNIQ    Name not unique on network
77  EBADFD  File descriptor in bad state
78  EREMCHG     Remote address changed
79  ELIBACC     Can not access a needed shared library
80  ELIBBAD     Accessing a corrupted shared library
81  ELIBSCN     .lib section in a.out corrupted
82  ELIBMAX     Attempting to link in too many shared libraries
83  ELIBEXEC    Cannot exec a shared library directly
84  EILSEQ  Illegal byte sequence
85  ERESTART    Interrupted system call should be restarted
86  ESTRPIPE    Streams pipe error
87  EUSERS  Too many users
88  ENOTSOCK    Socket operation on non-socket
89  EDESTADDRREQ    Destination address required
90  EMSGSIZE    Message too long
91  EPROTOTYPE  Protocol wrong type for socket
92  ENOPROTOOPT     Protocol not available
93  EPROTONOSUPPORT     Protocol not supported
94  ESOCKTNOSUPPORT     Socket type not supported
95  EOPNOTSUPP  Operation not supported on transport endpoint
96  EPFNOSUPPORT    Protocol family not supported
97  EAFNOSUPPORT    Address family not supported by protocol
98  EADDRINUSE  Address already in use
99  EADDRNOTAVAIL   Cannot assign requested address
100     ENETDOWN    Network is down
101     ENETUNREACH     Network is unreachable
102     ENETRESET   Network dropped connection because of reset
103     ECONNABORTED    Software caused connection abort
104     ECONNRESET  Connection reset by peer
105     ENOBUFS     No buffer space available
106     EISCONN     Transport endpoint is already connected
107     ENOTCONN    Transport endpoint is not connected
108     ESHUTDOWN   Cannot send after transport endpoint shutdown
109     ETOOMANYREFS    Too many references: cannot splice
110     ETIMEDOUT   Connection timed out
111     ECONNREFUSED    Connection refused
112     EHOSTDOWN   Host is down
113     EHOSTUNREACH    No route to host
114     EALREADY    Operation already in progress
115     EINPROGRESS     Operation now in progress
116     ESTALE  Stale NFS file handle
117     EUCLEAN     Structure needs cleaning
118     ENOTNAM     Not a XENIX named type file
119     ENAVAIL     No XENIX semaphores available
120     EISNAM  Is a named type file
121     EREMOTEIO   Remote I/O error
122     EDQUOT  Quota exceeded
123     ENOMEDIUM   No medium found
124     EMEDIUMTYPE     Wrong medium type
125     ECANCELED   Operation Canceled
126     ENOKEY  Required key not available
127     EKEYEXPIRED     Key has expired
128     EKEYREVOKED     Key has been revoked
129     EKEYREJECTED    Key was rejected by service
130     EOWNERDEAD  Owner died
131     ENOTRECOVERABLE     State not recoverable

Upvotes: 0

AShelly
AShelly

Reputation: 35540

One thing I've realized is that it is often not worth the trouble to percolate fatal errors up the stack. If something fails in a way that makes forward progress impossible, just end the program there. I usually deal with it by creating an error_exit function that I can call from anywhere:

void error_exit(int code, const char* message) {
  printf("Error %d: %s\nExiting!\n", code, message);
  cleanup();
  exit(code);
}

float* nested_function(int input, ...) {
  if (causes_hopeless_failure(input)) {
    error_exit(err_HOPELESS, "invalid input to nested_function");
  }
  //normal processing proceeds ...
  return valid_pointer;
}

int main() {
   float* vector = function_which_eventually_calls_nested_function();
   cleanup();
   return 0;
}

The cleanup function is used to deal with resources that don't get cleaned up correctly on program exit. File handles and allocated buffers don't usually fall into this category. I generally use it for system configuration changes that need to be undone.

Upvotes: 0

KamilCuk
KamilCuk

Reputation: 140990

Don't return out of band error codes with pointers. A pointer is either NULL or valid (if it's not valid, usually stack overflowed in your program). Doing char *pointer = (char*)(uintptr_t)1; is just confusing, and doing if ((uintptr_t)pointer == 1) {.. } is as much un-maintainable.

Return an int. int is common in C standard library to return an error. Usually C libraries return -1 on error and set errno - I usually write library code that return a negative value that is an error code (ie. return -ENOMEM in case of malloc fail). Return 0 on success and maybe positive values to notify user codes on some "state" within the library. Pass all variables you want to set by a pointer. Look at ex. fopen_s (opinion: don't use fopen_s, only look at it).

enum importantFunction_rets_e {
   IMPORTANT_FUNCTION_ERR_1 = -1,
   IMPORTANT_FUNCTION_ERR_2 = -2,
   IMPORTANT_FUNCTION_STATE_1 = 1,
   IMPORTANT_FUNCTION_STATE_2 = 2,
};

int importantFunction(char **pointer)
{
   assert(pointer != NULL);
   // or maybe
   if (pointer == NULL) return -EINVAL;

   int ret;
   ret = secondFunction(pointer);
   if (ret < 0) return ret;
   ret = thirdFunction(pointer);
   if (ret < 0) return ret; 
   return 0;
}

int secondFunction(char **pointer) {
  *pointer = malloc(sizeof(char) * 5);
   if (*pointer == NULL) {
      return IMPORTANT_FUNCTION_ERR_1;
   }
   memcpy(*pointer, "hey!", 5);
   return 0;
}

int main() {
    char *pointer;
    const int importantFunction_ret = importantFunction(&pointer);
    if (importantFunction_ret < 0) {
       if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_1) {
          // handle err 1
       } else if (importantFunction_ret == IMPORTANT_FUNCTION_ERR_2) {
          // handle err 2
       } else {
          // hanlde other errors
       }
       return -1;
    }
    if (importantFunction_ret == IMPORTANT_FUNCTION_STATE_1) { 
         // handle state1
    } else if {importantFunction_ret == IMPORTANT_FUNCTION_STATE_2) {
        // handle state2
    } else {
        // handle other states 
        assert(0);
    }
}

If you feel like exploring the topic or error handling in C, you can implement something along the new (or is it old?) proposal that uses the same method as object oriented languages use std::variant or std::expected or similar (opinion: I am really against this proposal as it is right now, it needs redesign/refactoring, but it would be a BIG step forward for C).

Upvotes: 2

Nominal Animal
Nominal Animal

Reputation: 39326

There are three common patterns of returning an error from a function:

  1. Have the function return an int, with specific values indicating success and failure

    For example, returning EXIT_SUCCESS or EXIT_FAILURE from main() is the way the C standards recommend reporting success or failure of the entire process. (BSD variants have attempted to standardize some other codes; if your system has a <sysexits.h> header, you could use those. But do note that they are not "standard", just the closest thing we have to an agreement how a process can report error codes.)
     

  2. Reserve a specific return value for errors, and use a global or thread-local variable (usually errno) to describe the error

    Most standard C library functions do this, with functions returning int using -1 for the error, and functions returning a pointer using NULL to indicate the error.
     

  3. Use an extra parameter to point to an error indicator.

    This approach is common with code and interfaces deriving from Fortran. Often, the error indicator is optional, and may be left NULL if the caller is not interested in whether the result is valid or not.
     

My own rules are simple:

  • Prefer the second approach when writing a low-level library. It is familiar approach to those familiar with the standard C library.

  • Use the first approach for recoverable errors.

    Often, I combine it with the second one, using return 0; for success, and return errno; or return errno = EINVAL; etc. for errors. (The last one first assigns EINVAL to errno, and then returns EINVAL.)

  • Use the third approach when error state should be retained over a number of operations, or there is a structure whose state errors affect.


Let's look how these methods differ in practice.

A very common thing to do is to parse command-line arguments as numbers. Let's look at a case where the arguments are to be used as doubles, for some kind of calculation:

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    int     arg;
    double  val;    

    for (arg = 1; arg < argc; arg++) {
        if (sscanf(argv[arg], "%lf", &val) == 1) {
            printf("argv[%d] = %.6f\n", arg, val);
        } else {
            printf("%s: Not a number.\n", argv[arg]);
            exit(EXIT_FAILURE);
        }
    }

    return EXIT_SUCCESS;
}

The above uses sscanf() to convert a string. Unfortunately, it does not check for any trailing garbage, so it accepts for example 1.5k as 1.5. To avoid that, we can use a dummy character to detect trailing garbage:

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    int     arg;
    double  val;
    char    dummy;

    for (arg = 1; arg < argc; arg++) {
        if (sscanf(argv[arg], "%lf %c", &val, &dummy) == 1) {
            printf("argv[%d] = %.6f\n", arg, val);
        } else {
            printf("%s: Not a number.\n", argv[arg]);
            exit(EXIT_FAILURE);
        }
    }

    return EXIT_SUCCESS;
}

That works, because sscanf() returns the number of successful conversions, and we expect only the double conversion (%lf) to work, and the char conversion (%c) to fail.

Unfortunately, scanf family of functions does not check for overflow. If you supply a large enough number, it gets silently mangled. Not good. To avoid that, we can use strtod(). To make use simpler, we can put it in a separate function, parse_double(). But, how should that return the value, and the possible error? Which one of the following to implement?

/* Convert the initial double, returning the pointer to the rest of the
   string; or NULL if an error occurs. */
const char *parse_double(const char *src, double *to);

/* If the string contains exactly one double, convert it and return 0.
   Otherwise return a nonzero error code. */
int parse_double(const char *src, double *to);

/* Convert the string to a double as best as you can. If an error occurs, return 'errval'. */
double parse_double(const char *src, const double errval);

So, which one of these is the best?

The answer is, of course, it depends on the use case.

I've actually implemented all three (in separate programs), depending on which one has been the most appropriate one.

The first one is especially useful when the same function is used to parse input files, and/or we allow any number of doubles per parameter/line. It is very easy to use in a loop.

The second one is what I most often use in programs. Very often, I use

typedef struct {
    double  x;
    double  y;
    double  z;
} vec3d;

int parse_vector(const char *src, vec3d *to)
{
    vec3d  temp;
    char   dummy;

    if (!src || !*src)
        return -1; /* NULL or empty string */

    if (sscanf(src, " %lf %lf %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3 ||
        sscanf(src, " %lf %*[.,:/] %lf %*[.,:/] %lf %c", &temp.x, &temp.y, &temp.z, &dummy) == 3) {
        if (to)
            *to = temp;
        return 0;
    }

    return -1;
}

which allows one to specify a 3D vector on the command line using 1+2+3, 1/2/3, 1:2:3, or even '1 2 3' or "1 2 3" (the quotes are needed to stop the shell from splitting it to three separate arguments). It does not check for double overflow, so it is important to show the parsed vector in the output, so that the user can detect if their input was misparsed.

(The asterisk * in %*[.,:/] means the result of the conversion is not stored anywhere, and the conversion is not counted in the return value. [ is a conversion specifier, "converting" any and all of the characters in the list, terminated with a ] character. [^ is the inverse, "converting" any and all characters not in the list.)

Upvotes: 4

TDk
TDk

Reputation: 1059

A common way of handling errors in C is through return values.

Say a function f, on success, returns a pointer to a character string.

char *f();

Such functions, upon failure, would return the NULL pointer, which you can have by including some common header files (e.g. <string.h>).

Now say g is a function that given an integer, computes something and returns the integer result of the operation, but the function can fail (e.g. the parameter is invalid for the computation, who knows...). Then maybe you want to write it like so

int g(int i, int *result);

Here, i is the parameter to compute something, and result is a pointer to the variable you will use to store the result in. Now, why is g's return type int ? Well, it could be bool from <stdbool.h> but usually one uses int... The return value will be used as a boolean, g will return 0 upon failure and 1 upon success.

You can use these like so in a third function h

int h(int i) {
    char *str = f();

    if (str == NULL) {
        printf("f failed !\n");
        return 0; // f failed
    }

    printf("%s\n", str);

    int result;
    if (!g(i, &result)) {
        printf("g failed !\n");
        return 0; // g failed
    } else {
        printf("result = %d\n", result);
    }

    return 1; // h success
}

Upvotes: 1

Related Questions