Fundamentals 31 min read

Mastering Error Handling in C: Strategies, Code Samples, and Best Practices

This article provides a comprehensive guide to error handling in C, covering error classification, step‑by‑step handling procedures, return‑value and out‑parameter techniques, global errno usage, goto and setjmp/longjmp jumps, signal handling, program termination functions, assertions, and practical encapsulation patterns with full code examples.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Mastering Error Handling in C: Strategies, Code Samples, and Best Practices

Preface

The article summarizes common error‑handling techniques used in embedded C programming and describes the runtime environment for the examples.

1. Error Concepts

1.1 Error Classification

Errors are classified by severity (fatal vs. non‑fatal) and by interaction (user errors vs. internal errors). Fatal errors cannot be recovered and usually result in logging and program termination, while non‑fatal errors are often temporary (e.g., resource shortage) and can be retried after a delay.

1.2 Handling Steps

Typical error handling consists of five stages:

Detect a software error (e.g., division by zero).

Record the error using an indicator such as an integer or struct.

Detect the error by reading the indicator or receiving a report.

Decide how to handle the error (ignore, partially handle, or fully handle).

Recover from or abort the program.

The following C code illustrates these steps:

int func()
{
    int bIsErrOccur = 0;
    // do something that might invoke errors
    if(bIsErrOccur) // Stage 1: error occurred
        return -1;   // Stage 2: generate error indicator
    // ...
    return 0;
}

int main(void)
{
    if(func() != 0) // Stage 3: detect error
    {
        // Stage 4: handle error
    }
    // Stage 5: recover or abort
    return 0;
}

2. Error Propagation

2.1 Return Values and Out Parameters

C functions usually signal success or failure via return values. Common patterns include checking the result of malloc, getchar, or clock. While simple, this approach can reduce readability, increase code clutter, and provide limited error information.

To convey richer information, functions may return an enum status and use out‑parameters for additional data:

typedef enum {
    S_OK,
    S_ERROR,
    S_NULL_POINTER,
    S_ILLEGAL_PARAM,
    S_OUT_OF_RANGE,
    S_MAX_STATUS
} FUNC_STATUS;

#define RC_NAME(eRetCode) \
    ((eRetCode) == S_OK ? "Success" : \
    (eRetCode) == S_ERROR ? "Failure" : \
    (eRetCode) == S_NULL_POINTER ? "NullPointer" : \
    (eRetCode) == S_ILLEGAL_PARAM ? "IllegalParas" : \
    (eRetCode) == S_OUT_OF_RANGE ? "OutOfRange" : "Unknown"))

2.2 Global State Flag (errno)

Many Unix system calls set a global errno variable on failure. The variable is defined in <errno.h> and is thread‑local on modern systems. Typical usage pattern:

if (some_call() == -1) {
    // errno now holds the error code
    printf("Error: %s
", strerror(errno));
}

Before calling a function that may set errno, it is advisable to reset errno = 0 and check it only when the function indicates failure.

2.3 Local Jump (goto)

The goto statement can centralize cleanup code. Example for division by zero:

int main(void) {
    int dwFlag = 0;
    if (1 == dwFlag) {
    RaiseException:
        printf("The divisor cannot be 0!
");
        exit(1);
    }
    dwFlag = 1;
    double fDividend = 0.0, fDivisor = 0.0;
    printf("Enter the dividend: ");
    scanf("%lf", &fDividend);
    printf("Enter the divisor : ");
    scanf("%lf", &fDivisor);
    if (0 == fDivisor)
        goto RaiseException;
    printf("The quotient is %.2lf
", fDividend / fDivisor);
    return 0;
}

2.4 Non‑Local Jump (setjmp/longjmp)

setjmp

saves the current stack environment; longjmp restores it, acting like a non‑local goto. Example:

jmp_buf gJmpBuf;

void Func1() { printf("Enter Func1
"); }
void Func2() { printf("Enter Func2
"); }
void Func3() { printf("Enter Func3
"); longjmp(gJmpBuf, 3); }

int main(void) {
    int dwJmpRet = setjmp(gJmpBuf);
    printf("dwJmpRet = %d
", dwJmpRet);
    if (dwJmpRet == 0) {
        Func1();
        Func2();
        Func3();
    } else {
        switch (dwJmpRet) {
            case 1: printf("Jump back from Func1
"); break;
            case 2: printf("Jump back from Func2
"); break;
            case 3: printf("Jump back from Func3
"); break;
            default: printf("Unknown Func!
");
        }
    }
    return 0;
}

When used across functions, this mechanism can emulate exception handling.

2.5 Signal Handling (signal/raise)

Signals such as SIGFPE indicate asynchronous error conditions. A handler can be installed with signal and triggered with raise:

void fphandler(int dwSigNo) {
    printf("Exception is raised, dwSigNo=%d!
", dwSigNo);
}

int main(void) {
    if (SIG_ERR == signal(SIGFPE, fphandler)) {
        fprintf(stderr, "Fail to set SIGFPE handler!
");
        exit(EXIT_FAILURE);
    }
    double fDividend = 10.0, fDivisor = 0.0;
    if (0 == fDivisor) {
        raise(SIGFPE);
        exit(EXIT_FAILURE);
    }
    printf("The quotient is %.2lf
", fDividend / fDivisor);
    return 0;
}

For integer division, the signal may be raised repeatedly; converting the signal to its default handling or using setjmp/longjmp can avoid endless loops.

3. Error Handling Strategies

3.1 Termination (abort/exit)

Fatal errors can end the program with exit (normal termination) or abort (abnormal termination). exit runs registered termination handlers and flushes standard I/O streams, while _Exit and _exit skip handlers and may skip flushing.

int main(void) {
    printf("Using exit...
");
    printf("This is the content in buffer");
    exit(0);
    printf("This line will never be reached
");
}

Using _exit without flushing can discard buffered output.

3.2 Assertion (assert)

The assert macro checks conditions that should never fail. In debug builds it expands to a call to __assert, which prints file, line, function, and the failed expression before calling abort. Example:

void __assert(const char *assertion, const char *filename, int linenumber, const char *function) {
    fprintf(stderr, "[%s(%d)%s] Assertion '%s' failed.
", filename, linenumber,
            function ? function : "UnknownFunc", assertion);
    abort();
}

Assertions are appropriate for checking internal invariants (e.g., non‑NULL pointers) but not for validating user input.

3.3 Encapsulation

To reduce repetitive error‑checking code, wrap system calls in helper functions. Example of a safe fork wrapper:

pid_t Fork(void) {
    pid_t pid;
    if ((pid = fork()) < 0) {
        fprintf(stderr, "Fork error: %s
", strerror(errno));
        exit(0);
    }
    return pid;
}

Similarly, a generic error‑printing routine can be built using stdarg.h and syslog for daemon processes.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CError HandlingsignalAssertsetjmperrno
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.