Use Exceptions Rather Than Return Codes (Use Exceptions rather than returning Codes)
You can set an error flag or return an error code that the caller can check.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class DeviceController { ... public void sendShutDown() { DeviceHandle handle = getHandle(DEV1); // Check the state of the device if (handle != DeviceHandle.INVALID) { // Save the device status to the record field retrieveDeviceRecord(handle); // If not suspended, shut down if (record.getStatus() != DEVICE_SUSPENDED) { pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } else { logger.log("Device suspended. Unable to shut down"); } } else { logger.log("Invalid handle for: " + DEV1.toString()); } } ... } |
The problem is that it’s messy with the caller. The caller must check directly after the call. Unfortunately, it is easy to be forgotten. For that reason, it is better to throw an exception when you encounter an error. It makes the code cleaner and the logic is not obscured by error handling.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class DeviceController { ... public void sendShutDown() { try { tryToShutDown(); } catch (DeviceShutDownError e) { logger.log(e); } } private void tryToShutDown() throws DeviceShutDownError { DeviceHandle handle = getHandle(DEV1); DeviceRecord record = retrieveDeviceRecord(handle); pauseDevice(handle); clearDeviceWorkQueue(handle); closeDevice(handle); } private DeviceHandle getHandle(DeviceID id) { ... throw new DeviceShutDownError("Invalid handle for: " + id.toString()); ... } ... } |
The code is better because there are two messy relationships here, the algorithm for device shutdown and error handling, and now it’s separated.
Write Your Try-Catch-Finally Statement First
Try – Like transactions. Catch – Leaves the program and no problems occur during try.
Using try-catch-finally is a good way to start writing code when you’re writing code and throwing exceptions. This helps us define what users should expect, no matter what the code in try.
For example: Write a piece of code with the purpose of accessing files and reading some objects in order.
We start with Unit Test telling us the exception when the file does not exist:
1 2 3 4 5 | @Test(expected = StorageException.class) public void retrieveSectionShouldThrowOnInvalidFileName() { sectionStore.retrieveSection("invalid - file"); } |
This test motivates us to write a stub:
1 2 3 4 5 | public List<RecordedGrip> retrieveSection(String sectionName) { // dummy return until we have a real implementation return new ArrayList<RecordedGrip>(); } |
This test failed and it didn’t throw an exception. Next we change the reality, so that it tries to access an invalid file. This action throws an exception:
1 2 3 4 5 6 7 8 9 | public List<RecordedGrip> retrieveSection(String sectionName) { try { FileInputStream stream = new FileInputStream(sectionName) } catch (Exception e) { throw new StorageException("retrieval error", e); } return new ArrayList<RecordedGrip>(); } |
Test passes because it has caught an exception. Restructuring:
1 2 3 4 5 6 7 8 9 10 | public List<RecordedGrip> retrieveSection(String sectionName) { try { FileInputStream stream = new FileInputStream(sectionName); stream.close(); } catch (FileNotFoundException e) { throw new StorageException("retrieval error”, e); } return new ArrayList<RecordedGrip>(); } |
Use Unchecked Exceptions
Checked: are the exceptions that are checked at compile time. Unchecked are the exceptions that are not checked at compiled time. C # doesn’t have checked exceptions. Checked exceptions can sometimes be useful if you are writing a critical library: You must catch them. But in general application development the dependency costs outweigh the benefits.
Provide Context with Exceptions (Provide Context with Exceptions)
Each exception should provide context to identify the source and location of the error. In Java, you will get a trace from any exception. However, that stack does not tell you the meaning of the exception that failed. Create an error message and pass it to your exception. Mention the actions that cause the error and the type of error.
Don’t Pass Null
Returns null from a bad method, but passing null is even worse. Unless you are working with an API that hopes to overcome null, you should avoid passing null in your code whenever possible,
1 2 3 4 5 6 7 8 | public class MetricsCalculator { public double xProjection(Point p1, Point p2) { return (p2.x – p1.x) * 1.5; } … } |
What happens if passing a null parameter? calculator.xProjection(null, new Point(12, 13));
We will have a NullPointerException. Not fixed:
1 2 3 4 5 6 7 8 9 10 11 | public class MetricsCalculator { public double xProjection(Point p1, Point p2) { if (p1 == null || p2 == null) { throw InvalidArgumentException( "Invalid argument for MetricsCalculator.xProjection"); } return (p2.x – p1.x) * 1.5; } } |
There is another better alternative:
1 2 3 4 5 6 7 8 9 | public class MetricsCalculator { public double xProjection(Point p1, Point p2) { assert p1 != null : "p1 should not be null"; assert p2 != null : "p2 should not be null"; return (p2.x – p1.x) * 1.5; } } |
In most programming languages there is no way of dealing with passing a null with an accidental call. Because this is the case to approach Null transmissions by default. When you do you can code with the understanding that a null appears in the parameter list is a sign of the problem and ends it with the least careless error,
Conclusion
Clean code is readable, but it also needs to be strong. This is not a contradiction. We can write clean and powerful code if we find that error handling is a particular concern, and sometimes we can treat it as independent of our basic logic. To the extent that we do that, we need to interpret it independently, and we can take a big step forward in maintaining our code.