Planning to Fail
Imagine this — after weeks of hard work, you close your code review, push your changes to production, and leave the desk to refill your coffee. When you return a few minutes later, you’re greeted with an influx of notifications from Slack — not the kind where someone’s posted an interesting article, the kind where you’re being tagged directly. Tested against production, your code has broken some existing function or is failing to handle some edge case. You open your error monitoring service of choice and see the following cryptic message:
Uncaught RangeError: Maximum call stack size exceeded
Oof — time to dive into that stack trace. ??
Now imagine the user on the other side. At this moment, they’re trying to upgrade to the premium offering on the platform you helped develop. After painstakingly entering their payment information, they click on the Submit button and… nothing happens. Frustrated, they refresh the page, re-attempt the process in a different browser, and feel their blood pressure rise when nothing changes. They wonder:?Did I fill out the form wrong? Is the website just broken? What if my payment goes through but doesn’t register in the system?
There are two points illustrated here. One, that effective error handling is important both for the developer and the user. A thoughtful error message is better than a cryptic error message, which in turn is better than no error message at all. Second, no matter how time consuming it is for the developer to chase down a bug, the impact on the end user is usually more significant, partly because of the knowledge gap between the developer and user.
What is an error?
For all the grief they cause, errors are just objects (in most languages, including JavaScript, which will be my focus). In JavaScript, the generic?Error?object that all JS errors inherit from contains two properties with full cross-browser support:
We can use these properties to surface helpful information for debugging.
Most JS errors that occur in the wild are automatically generated from the JavaScript engine. There are?many types of JS errors, but it’s easiest to understand them as three categories (which also apply to most other languages):?syntax errors,?runtime errors, and?logical errors.
Syntax errors
These are analogous to “grammar” errors, caused by not following the proper rules and structure of the language (example:?SyntaxError: missing ) after argument list). Unlike humans, computers can’t ignore syntax mistakes, so dropping a parenthesis or misspelling a keyword like?var?will?stop the program from running. The good news is that modern editors (many on our team favor?Visual Studio Code) and linters like ESLint will catch these and warn you about them while you’re typing, so all you have to do is pay attention to the red squiggly lines and make the corrections needed to clear them. These are highly avoidable and easy to resolve, so we’ll focus more on the two other types of errors.
Runtime errors
As is implied by their name, these errors happen when your program is being executed. Maybe your code passed your team’s diligent QA, but in the wild, people are interacting with it in a way that causes the program to crash (ranging from bad input to simply using an old browser). As a result, (real life!) user experience is impacted on some level. The bad news is that it’s impossible for computers to detect run-time errors in advance. The good news is that we can anticipate exceptions and handle them in a way that is empathetic to the user?and?helps us understand the root cause of the failure.
How do we do this? By intercepting the error. If we expect a particular bit of code to be risky and might throw an exception, we can wrap it in a?try/catch/finally?statement (which has equivalents in many other languages).
The try/catch clause
try
someRiskyFunction() // Often an API request
} catch(error) {
displayErrorToUser (error)
} finally {
// Clean up if needed - this statement is often skipped
}{
The?try?clause runs a block of code, and the?catch?clause is executed if an exception is detected in the?try?clause. We can use?catch?to render a user-friendly version of the?error?in the UI. The less common, optional?finally?clause runs regardless of the outcome, and is usually used for some form of “clean up” (i.e. resetting the UI). Even though using the?try/catch?statement doesn’t magically fix the source of the exception, it allows us to maintain a normal-feeling flow for the user, even when something unexpected has occurred.
A good rule of thumb for both internal and external errors is to write messages that make sense from a user’s point of view (human-friendly output please!). You may deeply understand your app, but the next person to work on your code likely will not. The error should also be actionable, communicating what was expected vs. what was received.
领英推荐
Tracking errors in a central location
Another note on the?catch?clause — it’s great for logging the error to a central system. At?Course Hero, we use?Rollbar?to track runtime errors, which will fire Slack notifications once an error exceeds a pre-set threshold. Whatever library or system you choose, it’s important to have visibility to the exceptions that are occurring in order to maintain a great user experience — you can’t fix what you can’t see!
Leveraging the ?? of compilers
For languages like JavaScript which are are considered “weakly typed” or “untyped”, compilers like TypeScript can be hugely helpful for reducing runtime errors. If I try to supply a?string?to a method that expects a?number, the compiler will point out the error rather than silently coercing one type into the other. As a bonus, this reduces the number of unit tests we need to write!
Logical errors
These are perhaps the most elusive errors of all. Logical errors won’t cause the program to crash but will fail to produce the result you’re looking for because of a flaw in business logic. They’re the hardest to identify because they are context-specific — in other words, the engine simply doesn’t know what you’re trying to do. For example, imagine that you’re developing a game, and users are noticing that their points can dip below zero. Mathematically, this is reasonable, but in the context of?gaming, 0 is usually the lower limit of scores.
So if the errors are more abstract, how do we handle them?
We can define our?own?exceptions using?throw?statements (which can be used in combination with?try / catch?statements)! Instead of waiting for one of the standard error types to occur, we can explicitly?throw?our own exceptions to force the transfer of control to the?catch?block.
Throwing an error
try
if (requestedUnits > totalUnits){
throw new Error("Not enough units. Please choose a lower number to purchase");
} else {
deductMoney()
}
}
catch(e) {
alert('Error:', e) // Message in alert box: "Error: Not enough units. Please choose a lower number to purchase"
}{
In this highly-contrived example, we check that it’s possible to purchase a requested number of units before deducting the credits from the user’s account. Without throwing the exception, we may otherwise be promising the user a quantity of the item that can’t be fulfilled while taking their money for the full amount!
Test driven development
In addition to throwing our own explicit exceptions, we can also curb (and prevent) logical errors through test driven development. Whether you add a test case after discovering a bug or before launch, writing tests helps us account for different scenarios and helps ensure that future changes don’t break existing behavior. At Course Hero we’ve increasingly leaned into using tests to make sure our front ends pass muster; we use?React Testing Library?for testing individual React components and?Cypress?for testing application flows (and legacy non-React code). Whatever framework you use, testing helps ensure that the code conforms to your mental model (if your mental model is inherently flawed, you’ll want to clear that up first ??).
Final thoughts
Anticipating — maybe even embracing? — things going wrong is a paradigm shift that comes with experience and time. But, you can do it. You can befriend errors, understand them deeply, and use them to your advantage. Though they may sting in the moment (coding is not immune from its own emotional waves), errors give us valuable insight to improve user experience and save us from future stress.
Let’s plan to fail. ??
-----
To see this blog on our Course Hero Engineering Blog, please click?here.
Interested in making an impact? Check out our careers page!
Senior Watershed Specialist, CSMB, SCWP
3 年If this isn’t true, I dunno what is