6 ms·
At my work (hosted enterprise webapp), both the frontend and backend have both an `assert` function and an `assertAndContinue` function. `assert` always crashes
by alangpierce 8y ago
At my work (hosted enterprise webapp), both the frontend and backend have both an `assert` function and an `assertAndContinue` function. `assert` always crashes on failure, and `assertAndContinue` crashes in dev and logs an error and keeps going in prod. Each time we want to verify a runtime assumption, we decide which type of assert to use. We prefer `assertAndContinue` (and I push for it in code review), but there are reasons to use plain assert:
• In some cases, crashing is a better user experience than proceeding in an unknown state. Usually this is on the backend when data integrity is at risk, but could be on the frontend when we're at risk of making a server call with the wrong arguments. But usually, pretty much anything that doesn't crash is better than crashing.
• `assertAndContinue` requires that you have a reasonable fallback. If code is going to crash the next line anyway, there's no point in `assertAndContinue`. In most cases, it's easy, but sometimes it takes real engineering, like building in an error state into the UI component that skips downstream code. When there isn't an obvious fallback, using `assertAndContinue` is a judgement call based on the difficulty of implementing a real fallback, the likelihood of the bug actually happening, and severity if the bug does trigger.
- wvenable 8y agoI have a difficult time imagining anytime that assertAndContinue makes sense. If a fallback is necessary and recovery possible, I would have asserts throw and use normal exceptional handling for that.
- MaulingMonkey 8y agoExceptions are a control flow mechanism, not necessarily an error collection mechanism. I also frequently work with languages either without exception handling or with exception handling disabled for certain targets. Even when exception handling is enabled, it's a bit silly and heavyweight if you just need to short circuit a single conditional, loop, or function. Why throw when you can break or return? And when you do use nonfatal-but-logged exceptions, what do you put in the exception handler and what makes it different from assertAndContinue besides minor errata like being specialized for exceptions?
- wvenable 8y ago> I also frequently work with languages either without exception handling or with exception handling disabled for certain targets. In those cases, you definitely cannot use exceptions but I'm still not sure I'd use something like assertAndContinue. If an assertion has failed, something has gone terribly wrong and I can't imagine just continuing. > it's a bit silly and heavyweight if you just need to short circuit a single conditional, loop, or function. Why throw when you can break or return? You throw because it's an error condition, you break or return because that's a potential the normal flow of operation. It's not heavyweight because it's for a state that should potentially never happen. > And when you do use nonfatal-but-logged exceptions, what do you put in the exception handler and what makes it different from assertAndContinue besides minor errata like being specialized for exceptions? All exceptions are "fatal" to the current operation, however you define it. It could be fatal to the entire application, bringing it down completely. Or it could just be fatal to a single function, request, or even UI button click. In comparison, assertAndContinue() would continue the current operation in, by definition, an invalid state.
- MaulingMonkey 8y ago> If an assertion has failed, something has gone terribly wrong and I can't imagine just continuing. For most traditional uses of asserts, sure. There's plenty of edge cases you might not want to ship with but aren't fatal if they do ship however. In a game this might be something like missing leaderboard definitions. Sure, losing player scores sucks, but it's not as bad as crashing the game outright and losing their progress. It's worth crashing the game at QA time to force it to get fixed, it's worth silently logging in production to avoid even bigger losses to the player. I consider those checks a variation on assertions, and "assertAndContinue" sounds like exactly the sort of thing you'd use for exactly this kind of check. Maybe you don't call them assertions...? > You throw because it's an error condition, you break or return because that's a potential the normal flow of operation. It's not heavyweight because it's for a state that should potentially never happen. Exceptions take full stack traces in a number of languages. Even in those that don't, compilers usually optimize for the exception-free path and basically ignore the performance of the exception handling path. And just because it "should" never happen doesn't mean it doesn't happen frequently enough to cause perf bugs. One example: A title I worked on had significant framerate problems if you pawed at the screen just right. The cause? Exceptions thrown from system APIs on invalid touch/finger IDs when querying finger positions. Because they invalidated the IDs before finger up events could be processed. It could've returned an invalid state, returned the last known finger position for that ID in release builds, or done any number of faster error handling paths and it wouldn't have even been noticed as a problem. Instead, it threw, I doublechecked there wasn't any way to pre-query the finger IDs or process finger up events faster/in time, added a terrible catch statement, and just ate the performance hit. Exceptions are heavyweight. Sometimes not so terribly so that they're the wrong tool for the job, but sometimes they are. > All exceptions are "fatal" to the current operation, however you define it. It could be fatal to the entire application, bringing it down completely. This is the nonfatal-but-logged case I was specifically referring to. Again: What do you put there? > In comparison, assertAndContinue() would continue the current operation in, by definition, an invalid state. Any code invoking assertAndContinue has presumably taken measures to properly handle the continue path, likely by simply aborting the operation being asked of it. Just like any code that uses a try/catch construct presumably would.
- reggieband 8y agoOften an assert macro can be written using throw/raise as the mechanism but it changes the syntax and reduces some boiler plate. For example: try { someVar.someMethod(params); } catch (e) { throw new WhateverError("More detail here", e); } becomes: assert(someVar != null, "Unexpected null someVar"); assertAndContinue(params.val > 0, "Expected greater than 0 val", params.val); // etc. someVar.someMethod(params); It may not look very different but it adds a documentation like effect to the code. It lays out the potential failure cases before the code execution. Even if internally `assert` becomes `throw`.
- wvenable 8y agoBit what assertAndContinue and not just assert in the second line? What value is there in continuing in that case?
- aaaaaaaaaab 8y agoSo that managers can show pretty charts on board meetings about low crash numbers.
- wvenable 8y agoJust add a top-level exception handler that captures and logs everything but then restarts main() and never crash again! :)
- reggieband 8y agoIn some cases there are suspicious values that aren't necessarily incorrect. The person writing the code might think: "I don't expect this value but it won't cause an exception in the way a null reference will". You may want to catch these in development and if they show up, you investigate and there is no actual problem with the value then you can update the assertion to more accurately reflect the data.
- WalterBright 8y agoRecovery is never possible if the program has entered an unanticipated and unknown state.
- temac 8y agoSo assertAndContinue makes no sense, right? On my side I don't really see its point anyway. If the code is actually ready to handle such failures, this is merely an error trace, so it should just be called that.
- WalterBright 8y ago> assertAndContinue I would never allow such a construct in any code I was in charge of.
- laurentl 8y ago> assertAndContinue OTOH, it’s the perfect title for a sequel of Halt and Catch Fire.
- alangpierce 8y ago> this is merely an error trace, so it should just be called that It's stricter than an error trace because if it's encountered in a development/testing context, it's a hard error and the engineer must fix it to proceed. Plain error traces should only be used for expected failure scenarios (like a third-party service times out, possibly), not situations that would be considered "unexpected" to the developer.
- temac 8y agoBut is the fallback path for production even correctly tested, then? And if it is, then what becomes the point of the assertion?
- gizmo686 8y agoNever is a strong word. Suppose the clock app on my phone fails an assert. Technically, this means that the entire phone system entered an unknown state; but there is a good chance that the bad state is contained within the failing app. In this case, it would be possible to recover by killing and restarting the offending process. Sure, it is possible that the crash was actually caused by bad state in the kernel, or even in the baseband processor, so we can't say for certain if restarting the app actually put us into a good state. However, the approach of isolating failures at the program level seems to have been greatly succesful at improving the reliability of the overall system. It does not seem unreasonable that when a "single" program becomes sufficiently complex, it would not benefit from a similar containment.
- alangpierce 8y agoSome examples of fallback behavior where I don't think exceptions would help that much. * Of two arguments to a function, exactly one is supposed to be non-null (since they're different ways of specifying the same thing). If both end up being non-null, assertAndContinue and just use the first one and pretend the second is null. * When listing all items of type X in a folder, ask the server for all items and do an integrity check that they are indeed in that folder and have the expected type. If any items aren't like that, assertAndContinue and filter those items out and just display the valid ones.
- wvenable 8y agoWow, I'd definitely throw in both those cases. If someone passed a value to the first function and it ignored it, they're likely not getting the result they want which will propagate through the system and be even worse than just dying right there. Same with the second case.
- alangpierce 8y agoTo be clear, both are considered bugs and both are reported to our error reporting system as bugs that we will then investigate as if they were crashes. The question is whether it's better to immediately crash the page for the user or whether to proceed in a best-effort way. From our experience, it's very common that the crash is in an area of code unrelated to the primary set of actions the user is trying to take. Having some widget behave strangely in the corner of your screen is much less damaging than completely blocking the user from getting work done. Also, to be clear, both of these examples are in frontend code, where mistakes are very unlikely to cause lasting problems, since anything affecting data integrity would already need to go across the client-server boundary.
- wvenable 8y agoFront-end code (or a desktop app) is a bit different because you don't crash out in front-end situation. But I'd still throw and stop the current function/process and recover at a higher level than let the code continue to run. The user shouldn't be stopped from doing anything else or trying again.
- aidos 8y agoI use a similar pattern for edge cases. We work with weird data and often have processes to cleaning it - non essential but useful for optimisation. Sometimes we’ll have an assumption about the structure based on what we’ve seen before but we can’t be sure that will always be the case. So if the following code requires a certain format, we check the assumption, log if not met and then don’t continue with that cleaning step. More often than not we never see the exception on production.
- mping 8y agoBadly written assert?
- kartan 8y ago> assertAndContinue We had a similar setup. assertAndContinue will add a trace in memory. When the app crashes it will dump the trace to a file and quit. (similar to https://stackoverflow.com/questions/8233388/ios-crash-log-catch-debug-info-catch-and-send-via-email-to-the-dev-team https://stackoverflow.com/questions/8233388/ios-crash-log-ca...) On load the app checks if a dump file exists and sends the contents to our server. So we will get the last N assertions for any crash plus other info gathered in the exit routine. No fallback thou. Just continue until the crash happens, if it happens.
- sitkack 8y agoIn code that runs on your hardware, assert() should actually be drop_into_debugger() and then page an oncall eng with the url to attach their debugger to. The act of dropping into the debugger should cause a 502 back to the client with a GUID to track the exact issue.
- KeepFlying 8y agoWe have a similar pattern in my product (large Win32 app). We have "Assert" which throws a dialog in Debug then continue execution, but is removed in Prod. and then we have "IfFailedCrash" type asserts that immediately crash the app so that our crash reporting systems can kick in and report back with state information. (Though these can independently be turned off in prod, reverting them to a no-op.) We end up with a lot of issues where developers ignore the dialogs, so we are slowly moving to more "crash" checks to catch issues more quickly.