How to trap errors with protected evaluation¶
This page explains how to catch errors in q with protected evaluation: the two trap forms, how to choose a handler, how to signal your own errors, what a trap does not catch, common patterns, and how to capture a backtrace.
An error in q aborts the evaluation that raised it. At the console, q then suspends in the debugger. In a server, the error goes back to the client. In a script, q stops loading the file. Often you want something else: a default value, a log message, a cleanup step, or a clear message for the caller. Protected evaluation does this. It applies a function and, if the function signals an error, passes control to a handler that you supply. It is q's equivalent of try/catch in other languages.
q has two trap forms, both ternary forms of Apply:
| Form | Use for |
|---|---|
@[f; x; h] — Trap At |
a unary function f and its argument x |
.[f; args; h] — Trap |
a function f of any rank and a list of its arguments args |
If the application succeeds, the trap returns its result. If it fails, the trap returns what the handler h gives.
Trap a unary function¶
Use @[f; x; h] when f takes one argument. If h is a function, q calls it with the error message as a string:
q)@[{1+x}; 41; {"caught: ",x}]
42
q)@[{1+x}; `a; {"caught: ",x}]
"caught: type"
Trap a function of several arguments¶
Use .[f; args; h] when f takes more than one argument. Pass the arguments as a list, as you would to . Apply:
q).[{x+y}; (1;2); {"caught: ",x}]
3
q).[{x+y}; (1;`a); {"caught: ",x}]
"caught: type"
A wrong number of arguments is an error that the trap catches, and you get a rank error:
q).[{x+y}; 1 2 3; {"caught: ",x}]
"caught: rank"
Trap At on a binary function returns a projection
@[f; x; h] applies f to one argument. If f takes two, q does not signal an error. It returns f projected on x, and the handler never runs:
q)@[{x+y}; 1; {"caught: ",x}]
{x+y}[1]
Use . for any function that takes more than one argument.
Choose a handler¶
The third argument is either a function, which q calls only on failure, or a value, which q returns on failure.
A handler function¶
A handler function takes one argument, the error message as a string. Use it to log the error, build a message, or compute a replacement result:
q)@[{1+x}; `a; {-2 "failed: ",x; 0N}]
failed: type
0N
A fallback value¶
If you only need a default, pass the value itself:
q)@[{1+x}; `a; 0N]
0N
q).[+; "ab"; `ouch]
`ouch
A fallback expression is always evaluated, and first
q evaluates arguments from right to left before it applies anything. A handler that is not a function is therefore evaluated whether or not the trap fails, and before f runs. A trap is unlike try/catch in this respect:
q)@[string; 42; a:100] / expression: evaluated anyway
"42"
q)a
100
q)@[string; 42; {b::99}] / function: called only on failure
"42"
q)`b in key `.
0b
Use a constant or a function. Never use an expression with side effects.
A handler function is not evaluated in advance, but q still parses it with the rest of the expression. So a syntax error in a handler fails at once, even when f succeeds:
q)@[2+; "42"; {)}]
')
[0] @[2+;"42";{)}]
^
Signal your own errors¶
Use ' Signal to raise an error from your own code. Signal a symbol or a string. The handler always receives a string:
q)@[{'`neg}; 1; ::]
"neg"
q)@[{'"too big"}; 1; ::]
"too big"
Here the handler is :: (Identity), which returns the error string unchanged. q signals only symbols and strings. If you try to signal any other type, you get an stype error:
q)@[{'x}; 1; ::]
"stype"
A signal inside a validating function gives the caller one place to handle each failure:
q)checkPositive:{if[x<=0; '"must be positive"]; sqrt x}
q)@[checkPositive; 16; {"caught: ",x}]
4f
q)@[checkPositive; -1; {"caught: ",x}]
"caught: must be positive"
Handle some errors and re-signal the rest¶
A trap catches every error. To handle only the ones you expect, test the message in the handler and signal anything else again:
q)onlyType:{[e] $[e~"type"; 0N; 'e]}
q)@[{1+x}; `a; onlyType]
0N
q)@[{'"limit"}; 1; onlyType]
'limit
[0] @[{'"limit"}; 1; onlyType]
^
Do this whenever an unexpected error should not be hidden behind a default value.
Know what a trap does not catch¶
Errors in the arguments¶
A trap protects only the application of f. q evaluates x or args before the trap starts, so an error there is not caught:
q)@[{2+x}; "42"+3; `err]
'type
[0] @[{2+x};"42"+3;`err]
^
To protect the whole computation, move it inside the function:
q)@[{2+"42"+x}; 3; `err]
`err
Errors in the handler¶
If the handler signals an error, the trap itself fails. The error goes to the caller, the same as an error without a trap:
q)@[{1+x}; `a; {'"handler: ",x}]
'handler: type
[0] @[{1+x};`a;{'"handler: ",x}]
^
This is what makes re-signaling work. It also means a handler that logs to a closed handle, or builds a message from the wrong type, replaces the original error with its own. Keep handlers short.
Tell a failure from a valid result¶
A fallback value is ambiguous if f can also return it. A trap that returns 0N on failure does not tell you whether f failed or returned a null. To distinguish the two, return a flag with the result:
q)try:{[f;x] @[{(1b;x y)}[f]; x; {(0b;x)}]}
q)try[{1+x}; 41]
1b
42
q)try[{1+x}; `a]
0b
"type"
The first item says which case it is. The second is the result or the error message. Debug a remote process uses the same convention to return a backtrace from a server.
Apply common patterns¶
Trap each item of a list¶
To keep one bad item from aborting the whole list, put the trap inside the iteration. Project the trap on f and the handler, and apply it with each:
q)@[{1+x}; ; 0N] each (1;`a;3)
2 0N 4
Parse input with a default¶
Casting from a string with Tok ("J"$) returns a null for input it cannot parse, so you get no error to catch. Signal one yourself, and the trap can log the bad value and supply a default:
q)parseNum:{[s] @[{if[null r:"J"$x; '"not a number: ",x]; r}; s; {-2 x; 0N}]}
q)parseNum each ("12";"abc";"7")
not a number: abc
12 0N 7
Evaluate a q expression in a string¶
value evaluates a string as q. A trap catches both evaluation errors and syntax errors:
q)@[value; "6*7"; {"error: ",x}]
42
q)@[value; "6*`7"; {"error: ",x}]
"error: type"
q)@[value; "{"; {"error: ",x}]
"error: {"
An incomplete expression is not always an error. "1+" is a valid projection, so @[value; "1+"; h] returns +[1] and the handler does not run.
Do not evaluate untrusted strings
A trap stops a bad expression from crashing your process. It does not stop a harmful expression from running. If you pass user input to value, that user can run any q code, including code that deletes data or calls system. See security and management.
Handle a failed connection¶
hopen signals an error if it cannot connect. Trap it to report the failure or retry, without aborting the caller:
q)@[hopen; `::59999; {-2 "cannot connect: ",x; 0Ni}]
cannot connect: hop. OS reports: Connection refused
0Ni
The same applies to a query sent over an open handle. An error on the server comes back to the client as a signal, so @[h; "query"; handler] catches it.
Release a resource, then re-signal¶
q has no finally clause. To release a resource whatever happens, close it in the handler and signal the error again, then close it on the success path as well:
withFile:{[path;f]
h:hopen path;
r:@[f; h; {[h;e] hclose h; 'e}[h]]; / on failure: close, then re-signal
hclose h; / on success: close
r}
q)withFile[`:out.txt; {x "hello\n"; 42}]
42
q)@[withFile[`:out.txt]; {x "hi\n"; 1+`a}; {"failed: ",x}]
"failed: type"
The handler is projected on h so that it can close the handle, and it still receives the error message as its last argument. The caller sees the original error, and the file is closed in both cases.
Capture a backtrace with .Q.trp¶
A trap gives you the error message, but not where the error happened. To get the call stack as well, use .Q.trp in place of @, or .Q.trpd in place of .. The handler takes two arguments: the error string and a backtrace object, which .Q.sbt formats as text:
q)f:{`hello+x}
q).Q.trp[f; 2; {[e;bt] -2 .Q.sbt bt; e}]
[2] f:{`hello+x}
^
[1] (.Q.trp)
[0] .Q.trp[f;2;{[e;bt] -2 .Q.sbt bt; e}]
^
"type"
Frame [2] shows the function that failed, and the caret marks the failing operation.
.Q.trp takes a unary function. For a function of more than one argument, use .Q.trpd, or project the function down to one argument:
q).Q.trpd[{x+y}; (1;`a); {[e;bt] "error: ",e}]
"error: type"
q).Q.trp[{x+y}[1]; `a; {[e;bt] "error: ",e}]
"error: type"
The backtrace matters most where nobody is at a prompt: a server handling client requests, or a scheduled job. How to debug shows how to log it in production and how to return it from a remote process.
Trapped errors do not suspend
Inside a trap, q unwinds the stack to the trap instead of starting the debugger, even at the console. This is the error-trap mode: @ and . set mode 0 (abort), and .Q.trp sets mode 2 (collect a stack trace, then abort).
Next steps¶
- See where traps fit among q's other control structures in how to control execution.
- Diagnose errors that you do not trap with how to debug.
- Exit a batch script with a failure code when a trapped step fails: see exit from a script.
- Look up what an error message means in errors.
- Read the references:
.@Trap and Trap At,'Signal,.Q.trp,.Q.trpd, and.Q.sbt. - Read Q for Mortals §10.1.9 Protected Evaluation.