jammycakes

Joined 14 years ago

Karma
299
Submissions
95
Member since
Oct 10
[ my public key: https://keybase.io/jammycakes; my proof: https://keybase.io/jammycakes/sigs/vrd7Del8qi-li4kpqxXJbxtHxRowIeXHElqMaRm34G4 ]
View on Hacker News

Recent submissions

Comment · 3 years ago
That's a good approach if you can cleanly separate out the old code from the new code, and if you can make sure that you've got all the old functionality behind the switch. Unfortunately this can be difficult at times. Feature toggles involving UI elements, third party services or legacy code can be difficult to test automatically, for example. Another risk is accidental exposure: if a feature toggle gets switched on prematurely for whatever reason, you'll end up with broken code in production.

The cases where I've experienced problems with feature toggles have been where we thought we were swapping out all the functionality but it later turned out that due to some subtleties or nuances with the system that we weren't familiar with, we had overlooked something or other.

Feature toggles sound like a less painful way of managing changes, but you really need to have a disciplined team, a well architected codebase, comprehensive test coverage and a solid switching infrastructure to avoid getting into trouble with them. My personal recommendation is to ask the question, "What would be the damage that would happen if this feature were switched on prematurely?" and if it's not a risk you're prepared to take, that's when to move to a separate branch.

Comment · 3 years ago
This incident highlights a problem that is often overlooked in the debate about feature branches versus feature toggles.

I've worked with both feature branches and feature toggles, and while long lived feature branches can be painful to work with what with all the conflicts, they do have the advantage that problems tend to be uncovered and resolved in development before they hit production.

When feature toggles go wrong, on the other hand, they go wrong in production -- sometimes, as was the case here, with catastrophic results. I've always been nervous about the fact that feature toggles and trunk based development means merging code into main that you know for a fact to be buggy, immature, insufficiently tested and in some cases knowingly broken. If the feature toggles themselves are buggy and don't cleanly separate out your production code from your development code, you're asking for trouble.

This particular case had an additional problem: they were repurposing an existing feature toggle for something else. That's just asking for trouble.

Comment · 3 years ago
It isn't in practice. Only a minority of methods actually need it.

It's certainly far, far better than having to add exactly the same check after every method call. Which is only what you need to do if you're working in a situation where exceptions are not an option.

Comment · 3 years ago
Usually, no you don't. You only write a try ... catch or try ... finally block round the entire method body, from the point where you create the resources you may need to clean up to the point where you no longer need them. For example:

    var myFile = File.Open(filename);
    try {
        while ((var s = file.ReadLine()) != null) {
            var entity = ProcessLine(s);
            // do whatever you need to do to entity
        }
    }
    finally {
        myFile.Dispose();
    }
C# gives you the using keyword as syntactic sugar for this:

    using (var myFile = File.Open(filename)) {
        while ((var s = file.ReadLine()) != null) {
            ProcessLine(s);
            // do whatever you need to do to entity
        }
    }
Comment · 3 years ago
You clean up processing that your own method is responsible for. For example, rolling back transactions that it has started, deleting temporary files that it has created, closing handles that it has opened, and so on and so forth. You rarely if ever need to know what kind of exception was thrown or why in order to do that.

You can only assume that the methods you have called have left their own work in a consistent state despite having thrown an exception. If they haven't, then they themselves have bugs and the appropriate cleanup code needs to be added there. Or, if it's a third party library, you should file a bug report or pull request with their maintainers.

You don't try to clean up other people's work for them. That would just cause confusion and result in messy, tightly coupled code that is hard to understand and reason about.

Comment · 3 years ago
> External services having gone offline, timeouts, and invalid user input are expected conditions you should handle locally.

Not necessarily. You should only handle expected conditions locally if there is a specific action that you need to take in response to them -- for example, correcting the condition that caused the error, retrying, falling back to an alternative, or cleaning up before reporting failure. Even if you do know what all the different failure modes are, you will only need to do this in a minority of cases, and those will be determined by your user stories, your acceptance criteria, your business priorities and your budgetary constraints. That is what I mean by "expected conditions." Ones that are (or that in theory could be) called out on your Jira tickets or your specification documents.

For anything else, the correct course of action is to assume that your own method is not able to fulfil its contract and to report that particular fact to its caller. Which is what "yeeting exceptions up the call stack" actually does.

> Almost everything else you listed represents a bug in your software that should terminate execution.

Well of course it represents a bug in your software, but you most certainly do not terminate execution altogether. You perform any cleanup that may be necessary, you record an event in your error log, and you show a generic error message to whoever needs to know about it, whether that be the end user or your support team.

Again, what action you need to do in these cases will depend on your user stories, your acceptance criteria, your business priorities and your budgetary constraints. But it is usually done right at the top level of your code in a single location. That is why "yeeting exceptions up the call stack" is appropriate for these cases.

You only terminate execution altogether if your process is so deeply diseased that for it to continue would cause even more damage. For example, memory corruption or failures of safety-critical systems.

> I’m more than a little shocked that you think yeeting exceptions up the call stack is appropriate for these cases.

I hope I've clarified what "yeeting exceptions up the call stack" actually does.

The alternative to "yeeting exceptions up the call stack" when you don't have any specific cleanup or corrective action that you can do is to continue execution regardless. This is almost never the correct thing to do as it means your code is running under assumptions that are incorrect. And that is a recipe for data corruption and all sorts of other nasties.

Comment · 3 years ago
> No; you simply abstract the underlying subsystem’s exceptions in your own types, the same way you do with any other type.

That's all very well as long as people actually do that. It doesn't always happen in practice. And even when they do, the abstractions are likely to be leaky ones.

> And yes, “railway oriented approaches” can absolutely do this.

How? Please provide a code sample to demonstrate how you would do so.

Comment · 3 years ago
If you want to (and are able to) document all possible failure modes, then checked exceptions will give you that. As far as I can tell, railway oriented approaches can't.

Unfortunately, you can only do that when the number of possible failure modes is fairly limited. In a complex codebase with lots of different layers, lots of different third party components, and lots of different abstractions and adapters, it can quickly become pretty unwieldy. And then you end up with someone or other deciding to take the easy way out and declaring their method as "throws Exception" which kind of defeats the purpose.

Comment · 3 years ago
Missing dependencies. External services having gone offline. Timeouts. Foreign key violations. Data corruption. Invalid user input. Incorrect assumptions about how a third party library works. Incorrectly configured firewalls. Bugs in your code. Subtle incompatibilities between libraries, frameworks or protocols. Botched deployments. Hacking attacks. The list is endless.

Probably not so much of an issue if you're dealing with well validated CAD data and most of your processing is in-memory using your own code. But if you're working with enterprise applications talking to each other via microservices written by different teams with different levels of competence, legacy code (sometimes spanning back decades), complex and poorly documented third party libraries and frameworks, design decisions that are more political than technical, and so on and so forth, it can quickly mount up.

Comment · 3 years ago
> The unfortunately missing part of exceptions (in mainstream languages) is that they handle this invisibly. Figuring out, at compile time, what sort of exceptions can appear inside a given function is not obvious.

Figuring out, at compile time, what sort of exceptions appear inside a given function is a futile exercise in many contexts, and railway oriented programming does not fix it. Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.

In any significantly complex codebase, the number of possible failure modes can be significant, many of them are ones that you do not anticipate, and of those that you can anticipate, many of them are ones that you cannot meaningfully handle there and then on the spot. In these cases, the only thing that you can reasonably do is propagate the error condition up the call stack, performing any cleanup necessary on the way out.

"Handling this invisibly" is also known as "convention over configuration." In languages that use exceptions, everyone understands that this is what is going on and adjusts their assumptions accordingly.

Comment · 3 years ago
The author followed up this post with another one a few years later titled "Against Railway Oriented Programming":

https://fsharpforfunandprofit.com/posts/against-railway-orie...

Railway-oriented programming is an interesting concept and it does have its use cases, but it does need to come with a massive health warning. I've often seen it used in practice to reinvent exception handling badly, and this is something I consider particularly ill advised because exceptions, when understood and used correctly, provide a much cleaner and more effective way of handling error conditions in most cases.

The thing about exceptions is that in most cases, they make the safe option the default. An error condition is an indication that your code can not do what its specification says that it does, and in that case you need to stop what you are doing, because to continue regardless means that your code will be operating under assumptions that are incorrect, potentially corrupting data. Error conditions can happen for a wide variety of reasons, many of which you do not anticipate and can not plan for, and in those cases the only safe option is to clean up if necessary and then propagate the error up to the caller. Exceptions do this automatically for you by default (you need to explicitly override it with a try/catch block) but alternative approaches, such as railway oriented programming, require you to add in a whole lot of extra boilerplate code that is easy to forget and easy to get wrong. If you can't handle the error condition on the way up the call stack, you would then log it at the top level and report a generic error to the user.

Having said that I see two particular use cases for this kind of technique. The first is situations where you need to handle specific, well defined and anticipated errors right at the point at which they occur. Validation is one example that comes to mind; another example is where you are trying to fetch a file or database record that does not exist. The second is situations where exception handling is not available for whatever reason. Asynchronous code using promises (for example with jQuery) are pretty much an exact implementation of railway oriented programming, but since modern JavaScript now has async/await, we can now use exception handling in these scenarios.

Comment · 4 years ago
> I think a much better pattern would be to enable dev teams to self serve. Set up the required infrastructure and guard rails, then let teams handle their own deployments and infrastructure.

I think that's how DevOps is actually supposed to be done in the first place. You view Ops -- and the code used to manage and support it -- as a product, and get a specialised team of experienced Devs (and architects) to build it.

Once you've got the basic infrastructure and architecture in place, you then train up the individual development teams to customise it, extend it and troubleshoot it as they need to. In much the same way as they do with any other software product.

Comment · 4 years ago
In a previous job, I joined a team that was supposed to be introducing DevOps to the organisation.

It started out well -- we spent a few months hacking with Terraform, Docker, Vagrant, Kubernetes, and related technologies to implement an infrastructure-as-code approach -- automating the process of provisioning and decommissioning servers, and building a setup where development teams could deploy updates with a simple git push.

Unfortunately it all went downhill fairly rapidly. We ended up spending the majority of our time manually applying security patches to a bunch of snowflake servers that we'd lifted-and-shifted from another hosting provider to AWS, and fielding support requests from the development teams. Within a year, we were being told in no uncertain terms by our project manager that we were an operations team, not a development team.

It felt like a complete bait-and-switch. Within two years, I had left the organisation in question and moved on to a new job elsewhere doing actual development again. Last I heard, the entire team had been disbanded.

It sounds like the author of this article must have had a very similar experience. I wonder just how common it is. It seems that in many places, "DevOps" is all Ops and no Dev.

Comment · 5 years ago
The big problem that I have with Clean Code -- and with its sequel, Clean Architecture -- is that for its most zealous proponents, it has ceased to be a means to an end and has instead become an end in itself. So they'll justify their approach by citing one or other of the SOLID principles, but they won't explain what benefit that particular SOLID principle is going to offer them in that particular case.

The point that I make about patterns and practices in programming is that they need to justify their existence in terms of value that they provide to the end user, to the customer, or to the business. If they can't provide clear evidence that they actually provide those benefits, or if they only provide benefits that the business isn't asking for, then they're just wasting time and money.

One example that Uncle Bob Martin hammers home a lot is separation of concerns. Separation of concerns can make your code a lot easier to read and maintain if it's done right -- unit testing is one good example here. But when it ceases to be a means to an end and becomes an end in itself, or when it tries to solve problems that the business isn't asking for, it degenerates into speculative generality. That's why you'll find project after project after project after project after project with cumbersome and obstructive data access layers just because you "might" want to swap out your database for some unknown mystery alternative some day.

Comment · 7 years ago
The problem with explicit error handling is that it's all too easy to get it wrong (by forgetting to check the return value) and when it does go wrong, it goes wrong silently, introducing a risk of leaving you with corrupt data. In production.

The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton