Skip to content

How to debug q code

This page explains how to diagnose a runtime error in q: what the debugger gives you when execution suspends, how to move between call-stack frames and read the locals in each, how to resume or abandon the call, and how to capture the same information in production where there is nobody to type at a prompt.

When an expression typed at the console signals, q does not simply return an error — it suspends at the point of failure and gives you a prompt inside the failed frame. The whole call stack is still alive, so every argument and local variable can be inspected where it stands.

When q enters the debugger

At a console you need no setup. The error-trap mode is 1 (suspend) for console input by default, so an error drops you straight into the debugger:

q)calculate:{[v] base:10; v+base}
q)main:{[m] calculate m}
q)main `type_error
'type
  [2]  calculate:{[v] base:10; v+base}
                                ^
q))

The extra ) in the prompt is the signal that you are suspended.

Other contexts behave differently, and that is what \e is for. The mode is set per context:

Mode Behavior Applies to
0 abort, unwinding to the nearest trap sync message processing, and inside @/. trap
1 suspend and run the debugger console input, by default
2 collect a stack trace, then abort set by .Q.trp

\e does not change your console session

\e sets the mode used before async and HTTP callbacks run — not for what you type. \e 1 makes those handlers break into the debugger; \e 2 dumps the backtrace to the server console for an async message, or into the socket for HTTP. See error-trap modes.

Because console input is already mode 1, adding \e 1 before debugging interactively changes nothing.

For a sync request the mode is 0, so an error aborts and returns to the caller rather than suspending the server. To capture diagnostics there, use a programmatic trap.

Read a suspension

Three things identify where you are:

  • The prompt. Each trailing ) is one level of suspension — q)) is one, q))) is two.
  • The caret. ^ points at the primitive that failed.
  • The frame index. [2] is the stack frame, counting up from [0], the expression you entered.

.Q.bt[] prints the whole stack, innermost first, with >> marking the frame you are in. On entry that is the innermost frame — where the error happened:

q)calculate:{[v] base:10; v+base}
q)process:{[p] tag:`inner; calculate p}
q)main:{[m] label:`outer; process m}
q)main `type_error
'type
  [3]  calculate:{[v] base:10; v+base}
                                ^
q)).Q.bt[]
>>[3]  calculate:{[v] base:10; v+base}
                                ^
  [2]  process:{[p] tag:`inner; calculate p}
                                ^
  [1]  main:{[m] label:`outer; process m}
                               ^
  [0]  main `type_error
       ^

Two single-character commands move between frames, and each prints the frame it lands on:

Command Moves
` up — to the caller, an older frame
. down — to the callee, a newer frame
& redisplays the current frame

Walking up from the failure towards the call you made:

q))`
  [2]  process:{[p] tag:`inner; calculate p}
                                ^
q))`
  [1]  main:{[m] label:`outer; process m}
                               ^

And back down again:

q)).
  [2]  process:{[p] tag:`inner; calculate p}
                                ^

` at the outermost frame and . at the innermost have nothing to move to, so the frame simply stays where it is.

Inspect the locals in a frame

This is what the navigation is for. Each frame has its own arguments and local variables, and naming one reads it from the frame you are currently in. The frame is the scope.

Starting at the failure in calculate, its argument and local are both in view:

q))v
`type_error
q))base
10

Move up one frame and the names change with it — process has its own argument p and local tag:

q))`
  [2]  process:{[p] tag:`inner; calculate p}
                                ^
q))tag
`inner

Up again, into main:

q))`
  [1]  main:{[m] label:`outer; process m}
                               ^
q))label
`outer

So the way to find where a bad value came from is to walk up the stack reading the same conceptual value at each level, until you reach the frame where it was still correct.

Two more variables describe the failure itself, wherever you are in the stack:

q).z.ex        / the primitive that failed
+
q).z.ey        / the arguments it was applied to
`type_error
10

.z.ex and .z.ey together often identify the bug without any navigation at all: here + was applied to a symbol and a long.

See the function you are inside

.z.s is the function executing in the current frame, returned as a value. It follows you as you navigate, so walking the stack hands you each definition in turn:

q))`
  [2]  middle:{[p] inner p}
                        ^
q)).z.s
{[p] inner p}

That matters because the source is not always available. The function may have been defined at a prompt, sent over IPC, or generated at run time; or you may be on a machine that does not have the repository checked out. The backtrace shows a source line only when q can find the file — .z.s gives you the definition regardless.

Being a value, it composes. value on it exposes the internals, of which the most useful is the defining script — the third element from the end, so take it relative to the length rather than by a fixed index:

q))first -3# value .z.s
"/home/me/proj/analytics.q"

For a function defined at a prompt rather than in a file, that element is empty:

q))first -3# value .z.s
""
An obfuscated function stays obfuscated

.z.s does not recover source that was removed. In a frame belonging to a function loaded from an obfuscated script, the backtrace shows only the name and .z.s reports locked:

q)hidden `boom
'type
  [1]  (hidden)

q)).z.s
locked

The frame's locals are still readable, though, which is often enough to work out what went in.

Run any expression, not just variable names

A frame is not a read-only view. The debugger prompt is a full q prompt positioned inside the suspended frame, so anything you could type in a session works there — arithmetic on the locals, calls to other functions, even assignments:

q))base*2
20
q))type v
-11h
q))tmp:base*7      / a new name, for the rest of the session
q))tmp
70

That turns the debugger into the most effective tool q has for understanding code you did not write. q lines are dense, and a nested expression is far easier to understand by evaluating it outward from the middle than by reading it. Suspended inside the frame, you have the real arguments in scope, so you can take the expression apart a piece at a time.

Set a breakpoint by signalling an error

There is no breakpoint command, but you do not need one: any error suspends the frame, so inserting a deliberate error where you want to stop gives you a breakpoint. A bare undefined name is the usual choice, because it is quick to type and obvious in a diff:

score:{[xs]
  'break;                            / breakpoint: remove when finished
  sum {x*x} xs where xs > avg xs }

Calling the function now stops on that line, with the argument in scope:

q)score 1 8 3 9 4 7
'break
  [1]  score:{[xs]
  'break;
   ^
  sum {x*x} xs where xs > avg xs }

From here, build the next line up one piece at a time and watch what each step produces:

q))xs
1 8 3 9 4 7
q))avg xs
5.333333
q))xs > avg xs
010101b
q))xs where xs > avg xs
8 9 7
q)){x*x} xs where xs > avg xs
64 81 49
q))sum {x*x} xs where xs > avg xs
194

Six evaluations and the line explains itself: it squares the above-average elements and totals them. Reading it cold is considerably harder than that.

Signal a bare name, not a string or symbol

The error has to be raised inside the function for the frame to survive. A bare undefined name does that. An explicit signal of a string or a symbol unwinds to the top instead, and the locals are gone:

q)score 1 8 3 9 4 7      / with '"break"; or '`break; on that line
'break
  [0]  score 1 8 3 9 4 7
       ^
q)xs
'xs

Note the frame index: [0] is the call you typed, not the function body — there is nothing to inspect. Any genuine error works too, so 1+`a is an equally good breakpoint if you prefer something that cannot be mistaken for a deliberate signal.

Instrument the line instead

Where you can edit the source, there is a lighter alternative that needs no suspension at all. 0N! displays its argument and returns it, so inserting it into an expression prints the value passing through that point while leaving the result unchanged.

Instrument the same line at each stage:

score:{[xs] sum 0N! {x*x} 0N! xs where 0N! xs > 0N! avg xs }

One call now reports every step, and still returns the answer:

q)score 1 8 3 9 4 7
5.333333
010101b
8 9 7
64 81 49
194

Those are the same five values the debugger produced, in the same order — q evaluates right to left, so the printing runs from the innermost expression outwards. The last line is the function's result, not a trace.

Which to reach for:

  • 0N! when you can edit the source and want the whole sequence at once, or when the code runs somewhere a prompt is no use to you — inside peach, in a production process, or on every row of a long loop where stopping once tells you little.
  • A breakpoint when you do not know in advance what to look at. The frame lets you ask questions you only think of after seeing the state, which a fixed set of 0N! calls cannot.

Either way, take the instrumentation out when you are done: 0N! left in code writes to stdout on every call.

Resume or abort

A suspension is a paused call, so you can either supply a value and let it continue, or throw it away.

q)):20         / resume, returning 20 from the failed expression
q)):           / resume, returning the generic null
q))\           / abandon the call and clear one level of suspension

Resuming really does complete the original call, which makes it a quick way to test a fix:

q)r:main `type_error
'type
  [3]  calculate:{[v] base:10; v+base}
                                ^
q)):20
q)r
20

Nested suspensions unwind one level at a time

An error raised inside the debugger suspends again, giving q))). \ pops a single level, so repeat it until the prompt returns to q).

Trap errors in production

Interactive debugging needs somebody at a prompt. A production process instead has to record what happened and carry on, which is what .Q.trp is for: it applies a function, and on failure calls your handler with the error string and a backtrace object. .Q.sbt formats that object for logging.

safeRun:{[f;x]
  .Q.trp[f;x;{[err;bt]
    -2"Captured Runtime Error: ",err;
    -2"Stack Trace:";
    -2 .Q.sbt bt;}]};

/ execute with trap
safeRun[{x+`a};1]
Captured Runtime Error: type
Stack Trace:
  [3]  {x+`a}
         ^
  [2]  (.Q.trp)

  [1]  safeRun:{[f;x].Q.trp[f;x;{[err;bt] -2"Captured Runtime Error: ",err;-2"Stack Trace:";-2 .Q.sbt bt;}]}
                     ^
  [0]  safeRun[{x+`a};1]
       ^

The handler takes two arguments:

  • err — the error string, as ' signalled it
  • bt — the backtrace object, to be rendered with .Q.sbt

Parse errors are caught too

Errors from parse also reach a .Q.trp handler with location information, so a syntax error in dynamically built code is reported like any other runtime error.

Send the log somewhere useful

-2 writes to stderr, which is fine for a worked example. For a process whose logs are actually collected, emit through the Logging module instead — it adds severity, structure and routing. See print output.

Debug a remote process

To diagnose a failure on a server you cannot attach to, wrap its request handler so the backtrace comes back with the response. .z.pg handles synchronous requests, where the mode is otherwise 0 and the error would simply abort.

Server

/ start server on port 5001
\p 5001

/ return (0;result) on success, (1;backtrace) on failure
.z.pg:{[x]
  .Q.trp[
    {(0;value x)};                                     / try: execute the request
    x;                                                 / arg: the incoming query
    {[err;bt] (1;"Remote Error: ",err,"\n",.Q.sbt bt)} / catch: return the log
  ]
 }

/ a function that will fail
f:{{x*y}[x;3#x]}

The two-element reply is a convention, not a protocol q imposes: the first element says which case it is, so the client can tell a result from a diagnosis.

Client

q)h:hopen 5001
q)r:h"f `a"
q)r 0                  / 1 means the call failed
1
q)-1 r 1;              / the formatted backtrace
Remote Error: type
  [5]  f@:{x*y}
            ^
  [4]  f:{{x*y}[x;3#x]}
          ^
  [3]  f `a
       ^
  [2]  .z.pg@:{(0;value x)}
                  ^
  [1]  (.Q.trp)

  [0]  .z.pg:{[x].Q.trp[{(0;value x)};x;{[err;bt] (1;"Remote Error: ",err,"\n",.Q.sbt bt)}]}
                 ^

Returning backtraces exposes your source

A backtrace contains the text of the functions it passed through. That is exactly what makes it useful in development, and exactly why you would not return it to an untrusted client. Log it server-side instead, and reply with an error identifier the caller can quote.

Next steps