The first bug report I received on an open source package came from someone in a timezone I could not guess, running a version of PHP I had never tested, on a hosting setup I had never seen. The report said four words: it does not work. I spent two days reconstructing their environment before I understood the problem, and the fix took six minutes.
That experience changed how I write code. At work, a confused colleague walks over to my desk and asks. A stranger who installs my package has no desk to walk to, no chat channel, and no context for any decision I made. The code, the error messages, and the documentation carry the entire conversation.
Years of FinTech and SaaS work taught me to build for known users with known constraints. Open source taught me to build for unknown ones. These are the habits I took from that, and I now apply them to internal services as well.
Error Messages Carry the Support Load
Nobody runs a support desk for a package they downloaded for free, so the error message becomes the support desk. I judge every error I write by one question: could a stranger fix this problem without opening an issue? If the answer is no, I rewrite the message.
Early in my open source work, I returned errors that described what failed but not why or what to do next. Each vague error turned into an issue, and each issue cost me an hour of back and forth. After I rewrote the five most common errors, my incoming issues dropped within a month.
// Before: the user learns nothing
return errors.New("invalid config")
// After: the user learns what failed, what I received, and what I expect
return fmt.Errorf(
"config: timeout %q is not a valid duration, use a value like \"30s\" or \"2m\"",
cfg.Timeout,
)
A good error names the field, shows the value the user supplied, and states the accepted format. It costs ten extra seconds when I write it and saves a stranger an hour. I treat every error string as part of the public API, because users paste them into search engines and issue trackers.
Documentation Is Part of the Interface
Developers read documentation before they read code, and many never read code at all. I stopped treating the README as an afterthought and started treating it as the first screen of the product. A user who cannot run an example in five minutes usually leaves.
I write the quick start before I finalize the API. If the quick start needs more than a few lines, the API has a problem, and the docs tell me so before any user does. This ordering has fixed more awkward interfaces than any code review I have run.
Become a Sponsor
Partner with us as a sponsor and help support our mission while connecting your brand with our community. We offer valuable opportunities to showcase your organization and build meaningful partnerships.
I also document the decisions I rejected. A short section titled "What this package does not do" prevents a steady stream of feature requests I would decline anyway. It respects the reader's time too, because they learn in one minute whether the package fits their use case.
Keep Examples Runnable
Every code example in my docs compiles and runs. I extract examples into tests so the build fails when an example breaks. A broken example in a README costs trust faster than a bug in the code.
func ExampleLimiter_Allow() {
l := NewLimiter(2)
fmt.Println(l.Allow(), l.Allow(), l.Allow())
// Output: true true false
}
Go makes this easy with example functions that the test runner executes and godoc displays. In PHP, I get the same effect by running README snippets through a script in CI. The mechanism matters less than the rule: documentation that cannot fail the build will drift from the code.
Drift happens quietly. A user on version three follows an example written for version one, hits an error, and blames the package instead of the docs. I have done this to myself as a user of other libraries, and it taught me to enforce the rule on my own.
Defaults Decide What Most Users Get
Most users never change a default. They install the package, run the example, and ship. Whatever value I picked for a timeout, a retry count, or a buffer size becomes the behavior of every production system that depends on it.
I once shipped a client with no default timeout because I assumed users would set their own. Users did not, and the first hanging request in someone's production service became my problem. I now ship every network facing option with a conservative default and state the value in the first paragraph that mentions the option.
type Option func(*Client)
func WithTimeout(d time.Duration) Option {
return func(c *Client) { c.timeout = d }
}
func NewClient(opts ...Option) *Client {
c := &Client{timeout: 10 * time.Second, retries: 2}
for _, opt := range opts {
opt(c)
}
return c
}
I choose defaults that fail safe, not defaults that win a benchmark. A strict default that annoys a user once teaches them to read the docs. A permissive default that corrupts data teaches them to stop using the package.
Breaking Changes Cost Strangers More Than They Cost You
When I rename a function in a private service, I update every caller in one pull request. When I rename a function in a public package, I break code I cannot see, owned by people I cannot contact. That asymmetry shapes every versioning decision I make.
I follow semantic versioning strictly, and I deprecate before I remove. A deprecation costs me a few lines and one release cycle. It gives every user a warning, a replacement, and time to migrate on their own schedule.
// Deprecated: use FetchContext, which accepts a context.Context.
// Fetch will remain until v3.0.
func (c *Client) Fetch(url string) ([]byte, error) {
return c.FetchContext(context.Background(), url)
}
The same discipline improved my internal work. On the Standard Chartered migration, internal services that treated their APIs like public contracts caused far fewer integration incidents than services that changed shape without notice. Versions and deprecation windows cost almost nothing, and they remove an entire category of surprise.
Issues and Pull Requests Are Conversations With Strangers
Every issue I receive comes from someone who spent time on my software and hit a wall. Most arrive with too little information and a frustrated tone. I learned to reply with the specific question that unblocks me, not with a template that makes the reporter feel processed.
Become a Sponsor
Partner with us as a sponsor and help support our mission while connecting your brand with our community. We offer valuable opportunities to showcase your organization and build meaningful partnerships.
I added an issue template that asks for the version, the environment, and a minimal reproduction. The quality of reports improved immediately, and the two days of reconstruction work from my first bug report disappeared. A reproduction in the report is worth more than any amount of speculation afterward.
For pull requests, I review the idea before the code. A contributor who spent a weekend on a feature I decline deserves a fast, clear answer with a reason, because their time has real value. I thank them, explain the boundary, and point to where the work could fit.
Conclusion
Open source removed every shortcut I relied on in team settings. I could not explain a decision in a meeting, fix a misunderstanding over coffee, or patch a bad assumption before every consumer picked it up. The software had to explain itself to people I would never meet.
Those constraints produced better habits than any style guide. Clear errors, runnable docs, safe defaults, and careful versioning each treat the reader's time as the scarcest resource in the system. I now apply all four to internal services, where the strangers turn out to be teammates six months in the future.
Software outlives my memory of why I wrote it. The only context that survives is the context I put in the code, the messages, and the docs.