Lesson 7: Error Handling and Exceptions
Master Erlang's error handling philosophy, try-catch patterns, and defensive programming techniques for building fault-tolerant chat systems
Now that weโve structured our data with records, we need robust error handling to manage the unexpected situations that arise when processing user input, network connections, and chat messages. Erlangโs error handling philosophy differs fundamentally from most languages, embracing failure as a normal part of system operation.
Erlangโs Error Philosophy
Erlang follows the โlet it crashโ philosophy. Rather than defensive programming with extensive error checking, Erlang encourages processes to fail quickly and cleanly. Supervisors then restart failed processes, maintaining system stability. This approach simplifies code and improves reliability.
1> divide(X, Y) -> X / Y.2> divide(10, 2).5.03> divide(10, 0).** exception error: an error occurred when evaluating an arithmetic expression in function erl_eval:do_apply/6 (erl_eval.erl, line 680)The division by zero crashes the process. In traditional languages, youโd check for zero first. In Erlang, we handle this at a higher level.
Types of Exceptions
Erlang has three exception types, each serving different purposes:
1> throw(custom_error).** exception throw: custom_error2> error(badarg).** exception error: badarg3> exit(normal).** exception exit: normal- throw: For expected, recoverable errors (like validation failures)
- error: For runtime errors and programming mistakes
- exit: For process termination signals
Try-Catch Blocks
When you need controlled error recovery, use try-catch:
1> safe_divide(X, Y) ->1> try1> X / Y1> catch1> error:badarith -> {error, division_by_zero}1> end.2> safe_divide(10, 2).5.03> safe_divide(10, 0).{error,division_by_zero}The try-catch pattern allows selective error handling while maintaining clean code flow.
Pattern Matching in Catch Clauses
Catch clauses support sophisticated pattern matching:
1> process_input(Input) ->1> try1> validate(Input),1> transform(Input)1> catch1> throw:{validation_error, Field} ->1> {error, {invalid_field, Field}};1> error:function_clause ->1> {error, invalid_format};1> _:_ ->1> {error, unknown}1> end.Each catch clause matches specific error patterns, enabling precise error handling.
After Blocks for Cleanup
The after block ensures cleanup code runs regardless of success or failure:
1> read_file(Path) ->1> {ok, File} = file:open(Path, [read]),1> try1> file:read(File, 1024)1> after1> file:close(File)1> end.The file closes even if reading fails, preventing resource leaks.
Error Handling with Records
Combining error handling with our record structures from lesson 06:
1> -record(user, {id, name, email}).2> -record(result, {status, data, error}).3> create_user(Name, Email) ->3> try3> validate_email(Email),3> User = #user{id=generate_id(), name=Name, email=Email},3> #result{status=ok, data=User}3> catch3> throw:invalid_email ->3> #result{status=error, error=invalid_email}3> end.Records provide structured error responses, making error handling consistent across your application.
Defensive Return Values
Many Erlang functions return tagged tuples instead of throwing exceptions:
1> parse_message(Input) ->1> case is_valid_format(Input) of1> true -> {ok, process(Input)};1> false -> {error, invalid_format}1> end.2> handle_result({ok, Data}) ->2> process_data(Data);2> handle_result({error, Reason}) ->2> log_error(Reason).This pattern allows callers to decide how to handle errors.
Error Propagation
Sometimes you want errors to propagate up the call stack:
1> process_batch(Items) ->1> Results = lists:map(1> fun(Item) ->1> try1> process_item(Item)1> catch1> error:Reason -> {error, Item, Reason}1> end1> end,1> Items1> ),1> case lists:filter(fun({error, _, _}) -> true; (_) -> false end, Results) of1> [] -> {ok, Results};1> Errors -> {batch_errors, Errors}1> end.Stack Traces
Erlang provides detailed stack traces for debugging:
1> catch_with_trace() ->1> try1> error(something_bad)1> catch1> error:Reason:Stacktrace ->1> {error, Reason, Stacktrace}1> end.2> catch_with_trace().{error,something_bad, [{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,680}]}, {shell,exprs,7,[{file,"shell.erl"},{line,686}]}]}Stack traces help identify error sources in complex call chains.
Key Takeaways
- โLet it crashโ philosophy simplifies code by avoiding defensive programming
- Three exception types serve different purposes: throw, error, and exit
- Try-catch blocks provide controlled error recovery when needed
- Pattern matching in catch clauses enables precise error handling
- After blocks ensure cleanup code always runs
- Tagged tuples offer non-exception error handling
- Stack traces aid debugging complex error scenarios
- Error propagation patterns handle batch operations gracefully
Interactive Koans
Koans - Test Your Understanding
Fill in the blanks and press Enter or click Run to test your knowledge!
What catches a validation_error thrown exception?
What value makes this try-of clause return success?
๐ก Hint
What variable captures the stack trace in this catch?
What value gets returned from this nested error handling?
๐ก Hint
What keyword ensures cleanup runs even after an error?
What atom completes this error tuple pattern match?
Finished this lesson?
Mark it as complete to track your progress
This open source tutorial is brought to you by East Branch Software - we build reliable software systems.
Found an issue? Edit this page on GitHub or open an issue