Welcome to another edition of “Station Wagon Full of Tapes”. In this article I’ve highlighted an inaccurate function signature type definition, and how it may lead to subtle bugs (the worst kind).
Another day in the wild, we are browsing an area of code we are not familiar enough, however we need a way to measure Euclidean distance for creating a prototype. We are feeling lucky, because we just found an interface that contains a function signaling to do exactly what we need, and has comments associated with it too! (Lucky indeed)
A function, takes two points, and returns the distance, and an error, if any.
type GPS interface { | |
// MeasureDistance will measure Euclidean distance between two point | |
MeasureDistance(a Point, b Point) (distance int, err Error) | |
} |
We go for it, utilizing the function and continuing to build our prototype. We are handling our error properly too, as responsible engineers always do. After testing the happy path for the app, all seems ready for others to play with considering we are also handling our errors in a way we believe it is acceptable.
func main() { | |
... | |
gps := ... | |
distance, err := gps.MeasureDistance(...) | |
if err != nil { | |
errorLogger.Log(err) | |
errorCounter.add(1) | |
... | |
} | |
... | |
} |
Click “Deploy”, and we are off to our alpha testers.
Users are reporting issues, however our loggers are showing no signs of issues, neither the Grafana dashboard we set up for counter metrics. A nightmare case, where we expect an unknown unknown to occur, but not even having a log to base this off is one of the worst cases we hoped to never encounter.
After some debugging, we find out that MeasureDistance never actually returned an error value back. There were cases where the calculus would result in fatal errors but it was neither handled or sent back as a part of the return value.
func MeasureDistance() (distance int, err Error) { | |
someCalculus() | |
unexpectedDivideByZeroFunc() | |
// TODO: Send the errors back when the inner funcs start reporting them. | |
return distance, nil | |
} |
It is easy to chalk this up as an engineer mistake, and argue a code-review would catch this. However, this is not always true. Things sometimes get shipped, with “TODO:” comments every day, and that is okay, business needs sometimes do require fast iterations. The issue here, in my opinion, is not the fact the errors are not handled and sent back properly. The issue is that the function signature acted like it was being handled properly. If this definition did not include an error return value, we would have no problems on the caller side. The caller is not expecting an error to be handled, they can, in turn build their own handling as they wish. (In Go, this is a bit tricky as no try catch pattern available, however for the sake of the argument I believe this does justice.)
Here’s another example, this time in Python with typing support:
def adjust_state(state: State) -> None: | |
... | |
def process(val: int, state: Optional[State] = None) -> ProcessResult: | |
... | |
# A functionality that assumes state to be passed in. | |
assert state | |
adjust_state(state) | |
def app(): | |
process(10) | |
# ^ No typing errors here, no signals that process technically | |
# requires a state object to be defined before called. |
The first thing I tend to skim about a function is its signature. The arguments it requires, whether some of them are optional or not, the return values. I believe the highest signal to get from an unfamiliar codebase in shortest amount of time is through function definitions and their type signatures. I believe this is also one of the reasons as to why TypeScript is gaining more and more momentum against JavaScript in bigger codebases.
Please do take extra care of your function signatures. Time is of essence, and signals need to be gathered fast.
Thank you for reading “Station Wagon Full of Tapes”.