Łukasz Makuch

Łukasz Makuch

How exceptions broke my middleware

Recently I work a lot with a web framework called Laravel. It's something like the Java ecosystem and Ruby on Rails had a child in the PHP land.

A couple of weeks ago I was writing some ordinary, boring code. Much to my surprise, it didn't always work as expected. After quite a long debugging session (shame on me) I managed to track the problem down. My middleware was broken. It didn't run completely. Just the beginning was executed, but not the rest. How was that possible?

There was an exception which broke the control flow. It was a great reminder why do some new programming languages (like Elm) have no exceptions.

Let's visualize how Laravel middlewares work when there's a controller Ctrl and two middlewares MidA and MidB.

Request handling happy path

The simplified flow goes like this. The request is first passed to MidA. It's then passed to MidB, which finally passes it to Ctrl. The controller produces some response and returns it to MidB, which then returns it to MidA, which finally sends it to the browser. Easy-peasy!

Unfortunately, it gets a lot more complicated, not to say unmanageable, as soon as exceptions come into play. It's because they are often used to control how the application behaves in situations which are not so exceptional. We're actually talking here about expected, supported events like what happens when the user is not authenticated or when some entity doesn't exist.

Let's consider a case where Ctrl throws an exception. Exceptions are breaking control flow

The beginning of MidA is executed all the way to the moment when it calls the next middleware, that is MidB. MidB is executed all the way to the moment when it calls Ctrl. Ctrl then throws an exception none of the middlewares knows about. The execution never comes back to the middlewares. They are never finished.

After I faced this problem I decided to take a look at other people's code in the hope of finding a solution which wouldn't require rewriting the existing code. The only thing I found was to register an event handler (with the proper priority) and finish the broken control flow within this event handler. To be honest, I find this unbelievably complicated. The flow of control is broken into implicitly connected tiny pieces of code which are scattered all over the codebase. It's very hard to answer questions which seem to be trivial, like "is the second line of this method going to be executed?" or "which piece of code will run next?".

I used to be a big fan of exceptions. I read about them in Java books, learnt how to catch them. But with time I grew to dislike them. I've noticed the hidden GOTO. When I'm writing a function which may return 3 different values, I simply make it return 3 different values. I don't pick my favorite type just to make the two remaining ones jump somewhere else dressed as exceptions.

I value the explicit, clean control flow exception-less code gives me. It's simple.

From the author of this post

  • howlong.app - a timesheet built for freelancers, not against them!