Don't undo what you haven't done

August 20, 2024

Turn something off, do a thing, turn it back on. Seems so simple! But... you might be undoing something that you never did in the first place. You might be committing a catastrophic error.

(Prefer to watch instead of read? Check out the video version of this article on YouTube: Poppin Off: Don't undo what you haven't done)

A few weeks after publishing this, Nuno Maduro ran across this exact bug, and it took his site down. Check out his tweet here.

The problem

Let’s jump into the code. We’re working with a Laravel command where we have two actions: superSecret and notSecret. The goal is to log a message in superSecret that shouldn’t be visible and another in notSecret that’s okay to show.

{
protected $signature = 'app:run';
 
protected bool $loggingEnabled = true;
 
public function handle(): void
{
$this->superSecret();
 
$this->notSecret();
}
 
public function superSecret(): void
{
$this->log('Uh oh, you shouldn\'t see this');
}
 
public function notSecret(): void
{
$this->log('It\'s ok to see this.');
}
 
public function log($msg)
{
if ($this->loggingEnabled) {
$this->line($msg);
}
}
}
Code highlighting powered by torchlight.dev (A service I created!)

If we run this, we’ll see both messages:

Uh oh, you shouldn't see this!
It's ok to see this.

Let's first work on hiding the super secret message. We can do this by setting a flag to disable logging before calling the super secret method and then re-enabling it after.

{
protected $signature = 'app:run';
 
protected bool $loggingEnabled = true;
 
public function handle(): void
{
$this->loggingEnabled = false;
$this->superSecret();
$this->loggingEnabled = true;
 
$this->notSecret();
}
 
}

So if we run this now, we’ll see only the not secret message.

This solution isn't good enough. We don't want to be doing turning things off and on like this. It's error-prone and can lead to some pretty nasty bugs.

Often you'll see folks do this within a dedicated callback:

{
public function handle(): void
{
$this->withoutLogging(function () {
$this->superSecret();
});
 
$this->notSecret();
}
 
public function withoutLogging($cb)
{
// Turn logging off
$this->loggingEnabled = false;
 
try {
// Call the user function
$cb();
} finally {
// Turn logging back on
$this->loggingEnabled = true;
}
}
}

This is nice because it's wrapped up into a clean callback, and if there's an error we turn logging back on and then we throw the exception. So now if we run this, we'll see only the not secret message.

However, we just made a tragic mistake! Imagine we've turned off logging in some other class, or we've turned logging off twice:

{
public function handle(): void
{
// Turn it off
$this->withoutLogging(function () {
// Turn it off again!
$this->withoutLogging(function () {
$this->superSecret(); // This stays hidden
});
 
$this->superSecret(); // This gets revealed!
 
});
 
$this->notSecret();
}
}

Now we're in a situation where we've turned off logging twice but then turned it back on after we exit the first callback.

So we're going to leak the super secret message.

We've undone something that we never did in the first place.

The first time we enter this closure we set loggingEnabled to false:

{
public function handle(): void
{
$this->withoutLogging(function () {
$this->withoutLogging(function () {
$this->superSecret();
});
 
$this->superSecret();
 
});
 
$this->notSecret();
}
}
Code highlighting powered by torchlight.dev (A service I created!)

Then immediately after that we set loggingEnabled to false again:

{
public function handle(): void
{
$this->withoutLogging(function () {
$this->withoutLogging(function () {
$this->superSecret();
});
 
$this->superSecret();
 
});
 
$this->notSecret();
}
}

Then we exit the closure and we set loggingEnabled to true:

{
public function handle(): void
{
$this->withoutLogging(function () {
$this->withoutLogging(function () {
$this->superSecret();
}); // loggingEnabled set back to true
 
$this->superSecret();
 
});
 
$this->notSecret();
}
}

However, we shouldn't really reset it until we exit the second closure.

{
public function handle(): void
{
$this->withoutLogging(function () {
$this->withoutLogging(function () {
$this->superSecret();
});
 
$this->superSecret();
 
}); // where we should ultimately set loggingEnabled back to true
 
$this->notSecret();
}
}

The solution

The way around this is to cache the state of the logging at the beginning of the withoutLogging function, explicitly turn logging off, perform the action, and then set it back to the cached state instead of explicitly turning it on.

public function withoutLogging($cb)
{
$loggingEnabled = $this->loggingEnabled;
 
$this->loggingEnabled = false;
 
try {
// Make sure to return the callback in
// case anyone else is relying on it.
return $cb();
} finally {
// Restore it to the previous state. Not explicitly `true`.
$this->loggingEnabled = $loggingEnabled;
}
}

Now if we run this, we'll see only the notSecret message. We've fixed our problem!

The pattern in Laravel

This is a pattern that you'll see in many frameworks. For example, in Laravel, you'll see this in the withoutBroadcasting method and withoutEvents method.

public static function withoutBroadcasting(callable $callback)
{
$isBroadcasting = static::$isBroadcasting;
 
static::$isBroadcasting = false;
 
try {
return $callback();
} finally {
static::$isBroadcasting = $isBroadcasting;
}
}

It's a good indication that if a big framework like Laravel is doing this, it's probably a good idea to do it in your own code as well.

An alternative: returning an undo function

If you don't like the nesting of the closures, you can return an undo function. (I covered this pattern in Clean up after yourself.)

Here we'll update our code to save the result of the withoutLogging function to an $undo variable. After we handle the superSecret methods, we'll call $undo to put it back to whatever it was originally.

{
public function handle(): void
{
$undo = $this->withoutLogging();
 
$this->superSecret();
$this->superSecret();
 
$undo();
 
$this->notSecret();
}
}

We'll need to make a few updates to our withoutLogging function. First, we'll default the callback to null, so we can call it without passing anything in. Then we save the current state of loggingEnabled to a variable.

Lastly, we'll return the undo function at the end of the method. The undo function will set loggingEnabled back to whatever it was originally.

public function withoutLogging($cb = null)
{
$loggingEnabled = $this->loggingEnabled;
$this->loggingEnabled = false;
 
$undo = function () use ($loggingEnabled) {
$this->loggingEnabled = $loggingEnabled;
};
 
if (is_null($cb)) {
return $undo;
}
 
try {
return $cb();
} finally {
$undo();
}
}

Now if we have some other distant class where we're doing and undoing something else, like logging, we will still respect who has done what and not undo someone else's action.

Wrapping Up

Don't undo what you haven't done. A simple but crucial pattern. Respect the state of the world when you enter a function, do your thing, and then return it back to the state it was in when you entered.

Youtube video

Me

Thanks for reading! My name is Aaron and I write, make videos , and generally try really hard .

If you ever have any questions or want to chat, I'm always on Twitter.

If you want to give me money (please do), you can buy my course on SQLite at HighPerformanceSQLite.com or my course on screencasting at Screencasting.com . On the off chance you're a sophomore at Texas A&M University, you can buy my accounting course at acct229.com .

You can find me on YouTube on my personal channel . If you love podcasts, I got you covered. You can listen to me on Mostly Technical .