Error Handling in Mathematical Functions
Mathematical functions such as sqrt() and pow() can fail, and C++ requires your code to check for those failures explicitly.
When Math Functions Fail
Some mathematical operations cannot produce a meaningful answer for every input. Taking the square root of a negative number is one example. Raising a very large number to a power is another, because the result can be too large to store as a double. C++ gives you a way to find out after the fact that one of these problems happened, but it does not stop your program on its own. Your code has to look for the problem and respond to it.
In this article you will follow a short piece of code that calls two mathematical functions and checks for one specific error condition after each call. The goal is not only to learn the rules but to practice watching the program move from line to line, so that you can see exactly when each check happens and what it reports.
Why errno Starts From a Clean Slate
Error state is kept in a value called errno. An error value from an earlier call can still be sitting in errno when you reach a later operation. If you skip the reset and check errno afterward, you may read that old error and conclude that the current operation failed, even when it did not.
The fix is to assign zero to errno before the operation you want to check. The source code does this with a comment that reads clear old error state, which describes its purpose exactly. After the reset, the next check reflects only the operation you are about to perform. Resetting errno to 0 before each operation means that a non-zero value afterward points to a problem that happened during that operation.
Matching Each Check to Its Function
The source uses two named constants, and each one identifies a different kind of failure. EDOM is checked after sqrt() to detect a domain error, meaning the input lies outside the values the function can accept. The example input sqrt(-1) is such a case. ERANGE is checked after pow() to detect a result too large to represent as a double. Raising the largest representable double to the power 2 is the example used in the source.
When you read a check, ask two questions: which function was called just before it, and which constant is being compared. A check against EDOM belongs after sqrt(). A check against ERANGE belongs after pow(). Using the wrong constant for a call means the check is looking for the wrong kind of problem.
Tracing the Error-Handling Sequence
Before you look at the trace, make a prediction. The sequence below calls sqrt() with a negative argument, then calls pow() with the largest representable double as its base and 2 as its exponent. Decide which of the two checks will find a problem and which message the program will print for each.
- errno = 0 clears old error state before the sqrt call
- EDOM is compared after
sqrt()becausesqrt()reports domain errors - errno = 0 is repeated so the
pow()check does not read the earlier result - ERANGE is compared after
pow()becausepow()reports results too large to store as a double handle_domain_errorandhandle_range_errorare placeholder names for whatever response your program needs
Finding where the output departs from the prediction
You predict that both checks will report a problem for the sequence above. Trace it step by step and find the first point where the behavior does not match your prediction.
- 1Step 1: first reset
errno is set to 0, so any earlier error is cleared before sqrt runs.
- 2Step 2: sqrt(-1.0)
The argument is negative, which lies outside the domain of
sqrt(). The check that follows is the EDOM check. - 3Step 3: EDOM check
If errno equals EDOM, the domain error handler runs. This is the first place a departure from the prediction can appear, so compare the reported result with what you expected here.
- 4Step 4: second reset
errno is set to 0 again. If this reset were missing, the ERANGE check would read whatever the first call left behind rather than the result of
pow(). - 5Step 5: pow and ERANGE check
The largest representable double raised to 2 can exceed what a double can store. The ERANGE check after
pow()is the one that should report it.
The departure from the prediction is found at a specific reset or comparison. Check at each line whether errno has been reset, whether the call has happened, and whether the comparison uses the constant that matches that call.
Changing the First Input
This variant is an original example created for practice. Suppose the first call is changed from sqrt(-1) to sqrt(4). The argument now lies inside the domain of sqrt(), so the call succeeds and returns 2. Predict what happens to the EDOM check that follows it.
What do you think happens?
After sqrt(4) returns 2, what does the EDOM check report?
Reveal answer
The EDOM check does not report a domain error, because errno was reset to 0 before the call and the call did not fail.
The reset means errno reflects only the sqrt() call. Since that call succeeded, errno is still 0 when the check runs, so the domain error branch is not taken.
Mistakes in Error Checks
Common mistake Checking errno without resetting it first
What it looks likeA check runs after a later math call, but errno still holds an error from an earlier call.
Why it failsThe check then reflects the old error rather than the current operation, so the program may believe the current call failed.
Do this insteadAssign errno = 0 immediately before the operation you want to check.
Common mistake Comparing against the wrong constant
What it looks likeChecking for ERANGE directly after sqrt(-1), or checking for EDOM after pow().
Why it failsEDOM identifies a domain error and ERANGE identifies a result too large to represent as a double. Each check looks for a different kind of problem.
Do this insteadCompare against EDOM after sqrt() and against ERANGE after pow().
Common mistake Assuming C++ stops the program when a math function fails. The source states that C++ does not stop your program on its own, so your code must look for the problem and respond to it.
Key Takeaways
- Some math operations, such as
sqrt()on a negative number orpow()with a very large result, can fail, and C++ does not stop the program on its own; your code must check for these failures. - Assign errno = 0 before each operation you want to check so that the check reflects only that operation and not an earlier error.
- After
sqrt(), the check looks for EDOM, a domain error. Afterpow(), the check looks for ERANGE, a result too large to represent as a double. - When tracing error-handling code, locate the exact reset or comparison where the behavior departs from your prediction.