Typed Errors Taught Me How to Treat Mistakes

I shipped a try-catch once that swallowed an error and returned a default. The default looked correct. The default was correct, by every test we had written. The default was the wrong default for a thing that happened to about one in two thousand requests, and I did not find out about the wrong default until a customer wrote in, three weeks later, with the kind of email that has been edited four times to remove the swearing, and I read the email at my desk, and I knew, before I had finished the second paragraph, exactly which try-catch he was emailing about, because I remembered writing it, and I remembered telling myself, at the time, that it was fine.

The try-catch was not fine. The try-catch was a small sentence I had written to my future self that read: I do not want to think about this right now, and you, future me, can deal with it when it becomes a problem. I had not realized I was writing that sentence. I had thought I was writing exception handling. I had been writing it, in various forms, for five years.

I want to write about what changed when I started using Effect, and I want to be careful, because I am not trying to sell you a library. I am trying to describe a shift in how I relate to a category of thing that I had been mishandling for my entire career, and the library happened to be the lever that made the shift visible, and the visibility was the part that mattered, not the library.

Here is the thing I learned. Errors are not interruptions. Errors are values. They are, just like the things that successfully come back from a function, information, and the information has a shape, and if you do not name the shape, you are choosing, in the moment, to throw the information away and to act as if the function returned a different value than it actually returned.

I had been writing code, for years, that pretended functions could not fail. The signature said getUser(id) → User. The reality was that getUser(id) could fail in about nine ways, and the signature acknowledged none of them, and the failure modes were therefore handled, when they were handled at all, by exception handlers wrapped around increasingly large blocks of code, far away from the function that actually knew what failure meant. The handlers did not know what to do with the failures, because they had no information about what kind of failure they were catching. They knew it was bad. They could log it. They could return a default. They could rethrow it, sometimes wrapped in something more polite. None of these were handling. They were variations on the same act, which was looking the failure in the face and choosing not to look.

When I write an Effect now, the signature does not lie to me. Effect<User, UserError> says, plainly, that this computation returns either a user or one of these specific kinds of errors, and the program will not compile if I do not handle, somewhere, all of them. I cannot pretend the call is total. I cannot wrap it in a try and catch the abstract failure I refused to name. I have to name them. UserNotFound. UserNotAuthorized. UserDatabaseTimeout. Each one is a value. Each one has a shape I can pattern-match on. Each one demands a decision I would have, in the old way, deferred forever.

The first month of writing in this style was uncomfortable in a way I want to describe accurately, because the discomfort is the part nobody warns you about, and the discomfort is also, I now think, the proof that the shift is real. The discomfort was that I could no longer write code at the speed I was used to. The speed I was used to was, in retrospect, partly the speed of being clear about what I was doing, and partly the speed of letting myself not be. The not-being-clear was where most of my speed actually came from. The compiler, with the error channel made explicit, was forcing me to be clear about every branch I had previously been allowed to leave vague, and being clear was slower, and the slower felt, at first, like the tool was wrong, and the tool was not wrong, the tool was holding up a mirror to a habit I had not known I had.

I want to slow down here, because I am about to do the thing this essay is actually about, which is to pivot from the technical to the personal, and I have to do it carefully, because the pivot is where most essays of this shape become unreadable.

Here is the pivot. I have been treating my own mistakes the way I used to treat exceptions. Caught, in some general handler that does not know what kind of failure it is dealing with. Logged, briefly, in some private place. Sometimes rethrown as something more polite. Often suppressed entirely, with a default that looks correct, that passes all the tests, that fails only quietly, far downstream, in some part of my life I will not be able to trace back to the original.

I have been doing this for thirty years. I learned to do it, I think, before I learned to talk. The mistake happens. The mistake is uncomfortable. The discomfort is what I treat, not the mistake. I find a default that lets me move on. The default looks like maturity. The default looks like resilience. The default is the same try-catch I used to write in code, applied to a person, and the person is me, and the failure mode is that the original information about what actually happened is lost.

If I had to name the kinds of mistakes I make most often, the way you would name an error type in a discriminated union, I could probably get to four or five. There would be MisreadingTheRoom. There would be CaringAboutBeingRightInsteadOfBeingClose. There would be UsingProductivityToAvoidGrief. There would be PerformingCertaintyWhereIShouldHaveAskedAQuestion. I am almost embarrassed to write them down, because writing them down makes them sound like personality flaws, and they are not personality flaws, they are recurring values that show up in the program of my life with predictable frequency, and the fact that I have been writing my life as if these values could not occur is a much more honest description of what is wrong than calling them flaws.

When I started naming them, the way I now name errors in Effect, two things happened, and both surprised me.

The first thing that happened was that the mistakes stopped feeling like ambushes. An ambush is a thing that happens to you. A typed value is a thing the function might return, that you have a place ready for. The mistake still happens. The discomfort still arrives. But the discomfort arrives into a small room I have prepared for it, and the room has a chair, and the chair has my name on it, and I sit in the chair, and the mistake and I look at each other, and we both already know what we are going to talk about, because I named the conversation in advance.

The second thing that happened was that the mistakes started teaching me things. The old try-catch, applied to my life, had been catching information and throwing it away. The new way, where I am forced to handle each named kind of mistake explicitly, means the information has somewhere to go. MisreadingTheRoom is teaching me, slowly, what I tend to misread. CaringAboutBeingRightInsteadOfBeingClose has shown me, in the last six months, three relationships I had been quietly losing without knowing why. The information was there the whole time. It had nowhere to go, so it went nowhere, so I learned nothing from it, so I made the same mistake again, and the same try-catch caught it, and the cycle ran for years.

I want to be careful not to oversell this. I am not, suddenly, a person who handles his mistakes well. I am a person who is starting to notice what he had been doing, and the noticing is what is changing things, slowly, in the way the mind changes when the language for a thing finally arrives. I still throw errors away. I still write defaults that look correct. I am still, on a bad week, wrapping my own life in a try-catch and shipping it.

But the try-catch is no longer the only tool I know about. There is another way. The other way is slower. The other way refuses to lie to me about what the function actually returned. The other way requires me to name the failures in advance, which means I have to know my own failures well enough to name them, which is its own work, and the work is, on most days, the actual work, and I had been letting the try-catch do it for me for thirty years.

Errors are values. Mistakes are information. Both have somewhere to go, if you give them somewhere to go. If you do not give them somewhere to go, they go nowhere, which means they go everywhere, which means they shape, silently, the program you are running, in ways you did not author and cannot debug, because you never gave them a name.

I am trying, now, to give them names.

— Dallen Pyrah