# Task 04: Error Handling **Phase**: 1 - Core (MVP) **Priority**: High **Estimated Time**: 1 hour **Dependencies**: Task 02 (Basic Execution) ## Objective Implement proper error detection and reporting for Elixir code execution, so users get meaningful feedback when their code fails. ## Prerequisites - Task 02 completed - Basic execution working ## Background Currently, Elixir errors are returned as raw output. We need to: 1. Detect when Elixir reports an error 2. Extract useful error information 3. Present errors clearly to the user 4. Optionally signal Emacs error conditions ## Steps ### Step 1: Define error patterns Add to `ob-elixir.el`: ```elisp ;;; Error Handling (defconst ob-elixir--error-regexp "^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)" "Regexp matching Elixir runtime errors. Group 1 is the error type, group 2 is the message.") (defconst ob-elixir--compile-error-regexp "^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)" "Regexp matching Elixir compile-time errors.") (defconst ob-elixir--warning-regexp "^warning: \\(.*\\)" "Regexp matching Elixir warnings.") ``` ### Step 2: Define custom error types ```elisp (define-error 'ob-elixir-error "Elixir evaluation error") (define-error 'ob-elixir-compile-error "Elixir compilation error" 'ob-elixir-error) (define-error 'ob-elixir-runtime-error "Elixir runtime error" 'ob-elixir-error) ``` ### Step 3: Implement error detection ```elisp (defun ob-elixir--detect-error (output) "Check OUTPUT for Elixir errors. Returns a plist with :type, :message, and :line if an error is found. Returns nil if no error detected." (cond ;; Compile-time error ((string-match ob-elixir--compile-error-regexp output) (list :type 'compile :error-type (match-string 1 output) :message (string-trim (match-string 2 output)) :full-output output)) ;; Runtime error ((string-match ob-elixir--error-regexp output) (list :type 'runtime :error-type (match-string 1 output) :message (string-trim (match-string 2 output)) :full-output output)) ;; No error (t nil))) ``` ### Step 4: Implement error formatting ```elisp (defun ob-elixir--format-error (error-info) "Format ERROR-INFO into a user-friendly message." (let ((type (plist-get error-info :type)) (error-type (plist-get error-info :error-type)) (message (plist-get error-info :message))) (format "Elixir %s: (%s) %s" (if (eq type 'compile) "Compile Error" "Runtime Error") error-type message))) (defcustom ob-elixir-signal-errors t "Whether to signal Emacs errors on Elixir execution failure. When non-nil, Elixir errors will be signaled as Emacs errors. When nil, errors are returned as the result string." :type 'boolean :group 'ob-elixir) ``` ### Step 5: Update the execute function Modify `ob-elixir--execute`: ```elisp (defun ob-elixir--execute (body result-type) "Execute BODY as Elixir code. RESULT-TYPE is either `value' or `output'. For `value', wraps code to capture return value. For `output', captures stdout directly. Returns the result as a string. May signal `ob-elixir-error' if execution fails and `ob-elixir-signal-errors' is non-nil." (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs")) (code (if (eq result-type 'value) (ob-elixir--wrap-for-value body) body))) (with-temp-file tmp-file (insert code)) (let ((result (org-babel-eval (format "%s %s" ob-elixir-command (org-babel-process-file-name tmp-file)) ""))) (ob-elixir--process-result result)))) (defun ob-elixir--process-result (result) "Process RESULT from Elixir execution. Checks for errors and handles them according to `ob-elixir-signal-errors'. Returns the cleaned result string." (let ((trimmed (string-trim result)) (error-info (ob-elixir--detect-error result))) (if error-info (if ob-elixir-signal-errors (signal (if (eq (plist-get error-info :type) 'compile) 'ob-elixir-compile-error 'ob-elixir-runtime-error) (list (ob-elixir--format-error error-info))) ;; Return error as result (plist-get error-info :full-output)) ;; No error, return trimmed result trimmed))) ``` ### Step 6: Handle warnings ```elisp (defcustom ob-elixir-show-warnings t "Whether to include warnings in output. When non-nil, Elixir warnings are included in the result. When nil, warnings are stripped from the output." :type 'boolean :group 'ob-elixir) (defun ob-elixir--strip-warnings (output) "Remove warning lines from OUTPUT if configured." (if ob-elixir-show-warnings output (let ((lines (split-string output "\n"))) (mapconcat #'identity (cl-remove-if (lambda (line) (string-match-p ob-elixir--warning-regexp line)) lines) "\n")))) ``` ### Step 7: Add tests Add to `test/test-ob-elixir.el`: ```elisp ;;; Error Handling Tests (ert-deftest ob-elixir-test-detect-runtime-error () "Test runtime error detection." (let ((output "** (RuntimeError) something went wrong")) (let ((error-info (ob-elixir--detect-error output))) (should error-info) (should (eq 'runtime (plist-get error-info :type))) (should (equal "RuntimeError" (plist-get error-info :error-type)))))) (ert-deftest ob-elixir-test-detect-compile-error () "Test compile error detection." (let ((output "** (CompileError) test.exs:1: undefined function foo/0")) (let ((error-info (ob-elixir--detect-error output))) (should error-info) (should (eq 'compile (plist-get error-info :type))) (should (equal "CompileError" (plist-get error-info :error-type)))))) (ert-deftest ob-elixir-test-no-error () "Test that valid output is not detected as error." (should-not (ob-elixir--detect-error "42")) (should-not (ob-elixir--detect-error "[1, 2, 3]")) (should-not (ob-elixir--detect-error "\"hello\""))) (ert-deftest ob-elixir-test-error-execution () "Test that errors are properly handled during execution." (skip-unless (executable-find ob-elixir-command)) (let ((ob-elixir-signal-errors nil)) (let ((result (ob-elixir--execute "raise \"test error\"" 'value))) (should (string-match-p "RuntimeError" result))))) (ert-deftest ob-elixir-test-error-signaling () "Test that errors are signaled when configured." (skip-unless (executable-find ob-elixir-command)) (let ((ob-elixir-signal-errors t)) (should-error (ob-elixir--execute "raise \"test error\"" 'value) :type 'ob-elixir-runtime-error))) (ert-deftest ob-elixir-test-undefined-function () "Test handling of undefined function error." (skip-unless (executable-find ob-elixir-command)) (let ((ob-elixir-signal-errors nil)) (let ((result (ob-elixir--execute "undefined_function()" 'value))) (should (string-match-p "\\(UndefinedFunctionError\\|CompileError\\)" result))))) ``` ### Step 8: Test in an org buffer Add to `test.org`: ```org * Error Handling Tests ** Runtime Error #+BEGIN_SRC elixir raise "This is a test error" #+END_SRC ** Compile Error #+BEGIN_SRC elixir def incomplete_function( #+END_SRC ** Undefined Function #+BEGIN_SRC elixir this_function_does_not_exist() #+END_SRC ** Warning (should still execute) #+BEGIN_SRC elixir x = 1 y = 2 x # y is unused, may generate warning #+END_SRC ``` ## Acceptance Criteria - [ ] Runtime errors are detected (e.g., `raise "error"`) - [ ] Compile errors are detected (e.g., syntax errors) - [ ] Errors are formatted with type and message - [ ] `ob-elixir-signal-errors` controls error behavior - [ ] Warnings are handled according to `ob-elixir-show-warnings` - [ ] Valid output is not mistakenly detected as errors - [ ] All tests pass: `make test` ## Error Types to Handle | Error Type | Example | Detection | |------------|---------|-----------| | RuntimeError | `raise "msg"` | `** (RuntimeError)` | | ArgumentError | Bad function arg | `** (ArgumentError)` | | ArithmeticError | Division by zero | `** (ArithmeticError)` | | CompileError | Syntax error | `** (CompileError)` | | SyntaxError | Invalid syntax | `** (SyntaxError)` | | TokenMissingError | Missing end | `** (TokenMissingError)` | | UndefinedFunctionError | Unknown function | `** (UndefinedFunctionError)` | ## Files Modified - `ob-elixir.el` - Add error handling functions - `test/test-ob-elixir.el` - Add error handling tests ## References - [docs/04-elixir-integration-strategies.md](../docs/04-elixir-integration-strategies.md) - Error Handling section