Secure Coding 101 – Error Handling and Logging

Errors are annoying; they cause unexpected behaviors and sometimes expose sensitive information if they are not handled well. In the second chapter of Secure Coding 101, we will explore effective error handling techniques and the importance of logging security events (Got some time? Check first article here.)

Errors: How They Occur and What They Really Are?

There are some types of errors, and they occur for various reasons. Here are a few common types of errors: syntax errors, logic errors, runtime errors, semantic errors, and others.

Errors are mistakes that happen because of programmers. Syntax errors occur when you forget a semicolon or do not follow the coding rules, leading to code that cannot be compiled or interpreted.

Logic errors arise when your algorithm is flawed, causing the program to produce incorrect results. Semantic errors happen when you forget a curly bracket or use a variable incorrectly, which can lead to confusion in the code’s meaning (and are actually quite similar to syntax errors).

So, what will we do when an error occurs? We watch it? Of course not! We will try to handle it without causing a crash. It may not be that simple to handle logical errors; if there is a logic error, you need to debug it and fix it rather than just trying to handle it.

So, what types of errors can you handle? You can handle runtime errors, input/output errors, network errors, resource errors, and arithmetic errors.

Syntax and semantic errors, on the other hand, do not need to be dealt with during execution because they prevent the program from running in the first place.

Handling Runtime Errors

Handling runtime errors is essential because, without addressing this type of error, your program will crash. In addition, it can lead to unexpected behaviors and create security vulnerabilities.

1. Try it and Catch it!

Try-catch blocks are a simple and effective way to handle runtime errors. You try a function or process, and if an error occurs, you catch that error and prevent unexpected behaviors. This allows you to act in a proper way instead of crashing the program.

#include <iostream>
#include <stdexcept> 

using namespace std;

int main() {
    int num;
    cout << "number: ";

    try {
        if (!(cin >> num)) {
            throw runtime_error("Invalid input."); 
        }
        cout << "You entered: " << num << endl;
    } catch (const runtime_error& e) {
        cerr << "Error: " << e.what() << endl;
    }

    return 0;
}

At this point, I assume you have a little knowledge of C++. In this code, we attempt to get an integer from the user. However, if the user provides a string instead of a number, it causes a runtime error. We catch this error and display an error message instead of crashing the program. (Of course, the code can be modified to handle input more gracefully, but the main point here is to demonstrate how try-catch works.)

Boring explanation: In C++, throw is used to signal that an error has occurred. When we use throw, we create an exception that can be caught by a catch block, allowing us to handle the error gracefully instead of crashing the program.

Basically; It screams that there is an error to make the catch block hear it and understand that there is an error.

On the other hand, cerr is a standard output stream used to display error messages. It outputs to the standard error stream, which is typically used for logging errors or warnings. Unlike cout, which is used for regular output, cerr is unbuffered, meaning it displays messages immediately, making it useful for error reporting.

So it’s so similar to cout function but without buffer so there is no delay to print error!

2. Input Validation

I made a long article about input validation, so I don’t want to go into too much detail here. However, while we demonstrate try and catch, we can see how to handle input effectively.

Here you can read; simply input validation is all about making sure the data users provide meets certain criteria before the program processes it.

3. Error Codes and Return Values

Hey, do you remember when we learned the first ‘Hello, World!’ program? We write ‘return 0;’ at the end of the main function. Do you ever think about why? Because it shows the program ended successfully.

So, in this logic, we can also use it for errors because an error means the program did not finish successfully. We can return the error code that indicates the error, and you can create a way to handle this.

#include <iostream>

int divide(int numerator, int denominator) {
    if (denominator == 0) {
        return -1;
    }
    return numerator / denominator; 
}

int main() {
    int result = divide(10, 0); 

    if (result == -1) {
        std::cout << "Division by zero!" << std::endl;
    } else {
        std::cout << "Result: " << result << std::endl;
    }

    return 0; 
}

As we know, in math, we can’t divide a number by 0. Instead, we throw an exception. We return -1 if the denominator is equal to 0 before it causes a crash, and later we use this returned value for handling the error.

It helps prevent crashes, but it works differently when we use try and catch. When we use try and cactch; we are trying to divide by 0, which causes an exception, and we handle this exception. But with error codes we are handling the input before it causes a crash.

4. Resource Management

In C or C++, you need to manage your resources on your own, and it can cause crashes, so be aware of what you use and what you don’t.

Don’t use more than you need, and don’t forget to clean them up when your job is done! Resource management is key to avoiding unexpected crashes.

Taking Notes to Fix It Later (Logging)

Logging, or I’m just saying taking notes for errors, is an important process for debugging. Not every time do we handle errors well, and there are new bugs in the code, so we should know what happened. We are taking notes of the situation to fix it later; it means logging.

#include <fstream>
#include <string>
#include <iostream>

using namespace std;

void logError(const string& errorMessage) {
    ofstream logFile("error.log", ios::app); 
    if (logFile.is_open()) {
        logFile << "Error: " << errorMessage << endl; 
        logFile.close(); 
    } else {
        cerr << "Unable to open log file." << endl; 
    }
}

int main() {
    logError("Division by zero attempted.");
    return 0;
}

We are creating an error.log file to keep track of errors, and later, when we encounter new errors, we record them. This process can be improved by adding more details, such as timestamps, error severity levels, and additional context about the errors.

#include <iostream>
#include <fstream>
#include <string>
#include <ctime>

using namespace std;

string getCurrentTimestamp() {
    time_t now = time(0);
    char* dt = ctime(&now);
    dt[strlen(dt) - 1] = '\0';
    return "[" + string(dt) + "]"; 
}

void log(const string& level, const string& message) {
    ofstream logFile("output.log", ios::app);
    if (logFile.is_open()) {
        logFile << getCurrentTimestamp() << " " << level << ": " << message << endl;
    }
}

int main() {
    log("INFO", "Program started.");
    log("ERROR", "An error occurred.");
    return 0;
}

// [Thu Feb 27 10:49:14 2025] INFO: Program started.
// [Thu Feb 27 10:49:14 2025] ERROR: An error occurred.

Customization is limitless. Improving logging depends on your needs and style. You can create a completely different logging format; it all depends on you.

Conclusion

In this second article, I explained error handling and logging in a way that I hope is easy for everyone to understand. However, if I made any mistakes, please feel free to inform me!

Handle your errors; otherwise, your code will suffer from crashes and vulnerabilities. That’s an easy conclusion for today!

I’m going to check this article and update it frequently. Have a nice day!

Leave a Reply

Your email address will not be published. Required fields are marked *