Clean Code Practices
Clean Code rules for error handling, concurrency, and unit testing
Tags
Overview
Purpose
Clean Code rules for error handling, concurrency, and unit testing
Rules
ERHA-001: Exceptions separate error handling from happy path, making code cleaner
Go uses (T, error) returns as its exception equivalent — same principle
ERHA-002: Use TDD to build tests that force exceptions, then add behavior to satisfy tests
Start with the error path in tests — ensures error handling is designed, not afterthought
ERHA-003: Checked exceptions cause cascade of changes through call hierarchy, breaking encapsulation
Go and TypeScript have no checked exceptions — use error types and Result<T> instead
ERHA-004: Create informative error messages mentioning operation and failure type
Every error should tell: what operation failed, what input caused it, what the failure type is
ERHA-005: Create wrapper that translates multiple exceptions into common type for your domain
Callers should not know about postgres driver errors — wrap into domain error types
ERHA-006: Return objects that handle special cases instead of throwing exceptions
Special Case pattern (Fowler) — expected outcomes are not exceptional
ERHA-007: Return empty collections or Special Case objects instead of null
Every null return forces callers to check nil, creating defensive boilerplate
ERHA-008: There is no good way to handle null passed accidentally; forbid by default
Use strictNullChecks in TypeScript and non-nil conventions in Go
CONC-001: Concurrency bugs are often not repeatable — treat spurious failures seriously
Don't dismiss intermittent test failures as 'one-off'
CONC-002: Get non-threaded code working first before adding concurrency
Debug logic bugs and concurrency bugs separately
CONC-003: Make threaded code pluggable for easy testing with different configurations
Tunability helps reproduce timing-dependent bugs
CONC-004: Run with more threads than processors to encourage task swapping
Forced context switches expose race conditions
CONC-005: Run on different platforms to find platform-specific issues
Thread scheduling varies by OS — what passes on Linux may fail on macOS
CONC-006: Instrument code to force different orderings and expose bugs
Deliberate jitter catches bugs that normal execution hides
CONC-007: Know your library's thread-safe collections and concurrency utilities
sync.Map, channels, sync.Mutex — use the right tool for Go concurrency
UNTE-001: Test code is just as important as production code
Tests are the safety net that enables everything else
UNTE-002: Dirty tests are equivalent to, or worse than, no tests
Hard-to-maintain tests get deleted — then you have no safety net
UNTE-003: Tests enable change — they eliminate fear of modifying code
Without tests, every change is a potential regression
UNTE-004: Tests enable the -ilities: flexibility, maintainability, reusability
Quality attributes emerge from test coverage
UNTE-005: Keep tests clean or you will lose them
Test rot leads to test abandonment
UNTE-006: Follow the Three Laws of TDD
1) Write failing test first 2) Write only enough test to fail 3) Write only enough code to pass
UNTE-007: Readability is paramount in tests — clarity, simplicity, density of expression
Tests are documentation — they should be the easiest code to read
UNTE-008: F.I.R.S.T. principles for clean tests
Fast, Independent, Repeatable, Self-Validating, Timely