The problem
You are running the same check in two places. One is a hook in the editor: before each save it looks at the change and warns about team rules. The other is a gate on a build server: before a command runs, it looks at the command and decides whether to let it.
The service goes down for half an hour. In the editor hook you had written a try/catch that swallows the error and carries on, so for thirty minutes every save passed unchecked and nobody knew. On the build server you had written "on error, block", so for thirty minutes every command was refused and the person on call spent the evening explaining why.
You go looking for the correct default and there isn’t one. Same judgment, opposite right answers. The editor hook has to let things through, because a tool that stops you working gets uninstalled by lunchtime. The gate has to hold things back, because nothing else stands behind it. The direction is not a technical question; it is a question about who you would rather inconvenience.
The solution
The fail open, fail closed pattern says: answer one question before you write the error handling — which costs more, an interruption or a miss? Then put that answer where you can see it, as a setting next to the call, not as a line buried inside a catch.
Make it a parameter, so the very same check reads "allow on error" in the editor and "deny on error" on the build server. It also stops the argument from happening twice: the direction is written down and can be pointed at.
Two habits go with it. When calls keep failing, stop making them for a minute and take the declared direction straight away instead of adding a three-second wait to every save; an authentication error should stop them on the very first try, since it is not going to fix itself. And count how many things went through, or were blocked, while the door was stuck that way — "it was down for half an hour" and "it was down for half an hour and waved through 3,800 messages" are different incidents.
Ask · Answer · Act
- AskSend the material and the questions as usual, plus two extra things: how long you are willing to wait, and which way to fall if nothing comes back.
- AnswerThree seconds pass and nothing comes back at all.
- ActTake the direction you declared. On this machine it is an unattended gate, so the command is refused and a line goes into the log; the same code inside an editor would have let the save through instead.
Where the name comes from
The name comes from locks. In a power cut a fire door has to spring open so people can get out, and a vault door has to lock shut so nobody can get in. Neither is the safe choice in general. You decide first who you are more afraid of trapping — or letting in — and then you pick the lock.
Use it when
Always. Every call already has a failure direction whether or not you wrote it down; this pattern only turns it into a line of configuration you can see and argue about.
Not when
Never. There is one disguise to watch for: "escalate to a person" at three in the morning, with nobody on call, is simply deny. Write it as deny and stop kidding yourself.
Projects that do this
- jev-axi — CLI for TypeSafe's Jev: fast calibrated judgments from the shell.
- mastra-jev-moderation — Mastra input moderation on Jev: one file, one typed request.
- foreman — Agent supervisor and software factory foreman using decision models.
Read this code
- jev-axi
src/commands/hook.ts:106-129The direction is a command-line switch: allow, ask or deny on error. The editor hook ships with allow, the unattended guard with deny — same judgment, opposite default.
- mastra-jev-moderation
jev-moderation.ts:232-241Three failures in a row stop the calls for sixty seconds; during that minute nothing is sent, and everything takes the declared direction with one line in the log.
- foreman
src/foreman/runtime.py:243-252A failed assessment becomes "escalate". Nobody is watching this one, so it fails towards stopping rather than towards carrying on.
Show me the code
async function guard(state, questions, onError) { // hook passes "allow", gate passes "deny"
try {
const { answers } = await jev(state, questions, { timeoutMs: 3000 });
return decide(answers);
} catch {
return onError; // timeout, rate limit, outage: all land here
}
}
const verdict = await guard(state, questions, "deny");
if (verdict === "deny") return refuse(command);How jevbooks recognises it
How do we know a project does this? We put one question about its README to Jev and it returns a percentage; the higher, the more it looks like it. If you copy the question, copy one thing: ask whether the text mentions this one concrete thing, never whether the project is good.
{
"failure-mode-knob": {
"type": "noul",
"instructions": {
"what": "Does the text state what happens when Jev errors or times out: allow, deny, escalate, circuit breaker?",
"not_for": "No stated behaviour on error."
},
"criteria": {
"true": "The text explicitly describes this. One sentence is enough; other content does not cancel it.",
"false": "The text does not describe this."
}
}
}