Debugging
General approach
Start from the innermost layer (core/database) and work outward to HTTP. Don’t debug through the full stack when you can isolate the issue.
1. Check logs
tail -100 logs/app.log | grep -A 10 "ERROR"
Errors are logged with stack traces. Look for the first non-Wagoe frame in the trace.
2. Add temporary logging
;; Output appears in REPL/server stdout, not log files
(println "DEBUG:" {:field value :other other-value})
Remove all println statements before committing.
3. Test via REPL
Bypass the HTTP layer and call services directly:
(def user-svc (get integrant.repl.state/system :wagoe/user-service))
(ports/find-user-by-email user-svc "alice@example.com")
4. Inspect HTTP requests
Add temporary logging in handlers:
(println "DEBUG request:"
{:method (:request-method request)
:params (:params request)
:headers (select-keys (:headers request) ["hx-request" "authorization"])})
5. Query the database directly
(def ds (get-in integrant.repl.state/system [:wagoe/db-context :datasource]))
(require '[next.jdbc :as jdbc])
(jdbc/execute! ds ["SELECT * FROM users WHERE email = ?" "alice@example.com"])
Common problems and solutions
nil where a field value is expected
Cause: snake_case / kebab-case mismatch.
Check: is the field :password_hash (snake) where the code expects :password-hash (kebab)?
Fix: always use cc/snake-case→kebab-case-map at the persistence boundary.
reset didn’t pick up changes
Cause: You changed a defrecord.
Fix:
(halt)
(go)
HTTP 500 with "Exception reached HTTP boundary without :type in ex-data"
Cause: An ex-info without :type, or an unwrapped Java exception.
Fix: wrap in try-catch with a typed error:
(try
(UUID/fromString id-string)
(catch IllegalArgumentException _
(throw (ex-info "Invalid UUID" {:type :validation-error :value id-string}))))
An error response carries a BND code — where does it come from?
In dev, an error over HTTP answers with a dev block:
{"error": "validation-error",
"message": "Request validation failed",
"details": {"email": ["missing required key"]},
"dev": {"code": "BND-201",
"category": "validation",
"docs-url": "bb guide error BND-201"}}
Three things have to be true for that block to appear, and each is deliberate:
-
:wagoe/dev-error-enricher {}is in the:activesection of the config —wagoe newwrites it into the dev config and nowhere else; -
the profile the app booted with is dev-like (
:dev,:test) — the profile, not an environment variable the deployment may never have been given; -
wagoe-devtoolsis on the classpath, and it lives in the:replalias — soclojure -M:run, the uberjar and the Docker image cannot produce it.
Production gets the same response without the dev block, and its details
name only the fields that were wrong. The full messages describe the schema —
"should be either \"admin\" or \"auditor\"" hands a caller every enum member
it never knew about — so they are dev-only too. A 5xx says "Internal Server
Error" and nothing else; the message, the class and the ex-data go to the
log, keyed by correlation ID. Delete the config key to see the production shape
while developing.
One thing this response does not go through: a coercion failure is answered before the interceptor stack runs, so it carries a correlation ID but not the security headers, and it is not counted by request metrics or the rate limiter.
SQL error: unknown column name
Cause: Schema and database migration are out of sync.
Fix: check that the field exists in both schema.clj, the migration SQL, and shell/persistence.clj.
Tests fail with unbalanced parentheses
Fix:
clj-paren-repair <file-with-issue>
Auth tests fail with "JWT secret not found"
Fix:
JWT_SECRET="dev-secret-at-least-32-characters-long" clojure -M:test :user