Skip to content

Logs

npx wawesome logs lists your Function’s recent runs, prints what one of them wrote, and follows output as it is produced. Each run is an invocation: a request arrives, your handler answers, the run ends. Concepts has the longer definition.

Every run captures whatever your code sent to stdout and stderr. That body is kept for 14 days.

npx wawesome logs with nothing after it reads the wawesome-function.json in the current directory. Pass a Function name to read one from anywhere.

npx wawesome logs charge
📜 Invocations for 'shop/charge' (Showing 4 of 4 records)

INVOCATION ID                        | STATUS    | TRIGGER | STARTED AT          | DURATION 
------------------------------------|-----------|---------|---------------------|----------
6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 | success   | http    | 2026-09-08 09:12:04 | 128ms    
a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6 | success   | http    | 2026-09-08 09:11:47 | 41ms     
2f8c5b91-7e04-4a3d-8b6c-1d9f4e2a0c53 | success   | manual  | 2026-09-08 09:07:15 | 1.42s    
ce03a6d4-19f7-4b8e-a2c5-6d81b0f9e374 | timeout   | cron    | 2026-09-08 09:00:00 | 30.01s   

To view logs for a specific invocation, run:
  wawesome logs --invocation <id>

To follow live output (waits for the next invocation if none is running):
  wawesome logs <function-name> --follow

To follow a specific invocation:
  wawesome logs --invocation <id> --follow

The 50 most recent runs, newest first. TRIGGER says what started each one: http for a caller, cron for a schedule, manual for a run you fired with invoke.

STATUS is not the HTTP status code. A run a caller pulled is success once it answered and survived its own body, whatever code it chose. So the second row above answered 500 and still reads success. error means we stopped the run before it finished. timeout means it ran past its budget, and running means it has not ended yet.

--error, --success, --timeout and --running filter on that column, and --status <status> takes the same four words. So --error finds traps and background runs that answered outside the 2xx range, and it never finds a 500 a handler returned to a caller.

An invocation id fetches the body that run captured.

npx wawesome logs --invocation a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6
{"ts":"2026-09-08T09:11:47.204118Z","stream":"stdout","msg":"Log: charging 4210 for cus_9f2"}
{"ts":"2026-09-08T09:11:47.241902Z","stream":"stderr","msg":"Error while running request handler: charges is not defined"}
{"ts":"2026-09-08T09:11:47.241940Z","stream":"stderr","msg":"Stack:"}
{"ts":"2026-09-08T09:11:47.241944Z","stream":"stderr","msg":"  fetch@./user.js:6:20"}
{"ts":"2026-09-08T09:11:47.241947Z","stream":"stderr","msg":"  @./index.js:5:42"}
{"ts":"2026-09-08T09:11:47.241951Z","stream":"stderr","msg":"  @./index.js:5:63"}

One JSON object per line. ts is when we read that line off your Function, to the microsecond, so a tight logging loop keeps its real order. stream is stdout or stderr. msg is the line itself.

The id goes in front of the flag just as well. That is the shorter thing to paste:

npx wawesome logs a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6

An id from another workspace, one that never existed, and one older than 14 days are all the same answer:

[wawesome] Error: Invocation log 'a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6' not found or expired (logs are retained for 14 days).

A row outlives the body it points at. The listing keeps a run for 35 days and its output for 14. A run from three weeks ago still has a row with its status and its duration, and nothing left to print.

Every response that became an invocation carries its own id back in the x-wawesome-invocation-id header.

HTTP/2 500
x-wawesome-invocation-id: a4d7e310-8b22-4c6f-91de-5f0c3a19b7e6

That is what makes a bug report worth having. Whoever hit the failing request reads the header off their own response and hands you one id. You read that exact run instead of guessing which of 50 rows was theirs.

Nearly all of it is your own output. The rest comes from the engine and from us.

The engine labels every console call with the method that made it, so console.log('ready') is stored as Log: ready. console.info, console.warn and console.error become Info:, Warn: and Error:. All four go to stdout, so a console.error of yours is not a stderr line.

The engine also writes when it catches something your code did not. That is the Error while running request handler: line above, and the Stack: block under it. Frames naming ./user.js are your module. That is the filename your entry point is deployed as, and the line and column are your own source. Frames naming ./index.js are the wrapper we generate around your module to route a trigger into your fetch handler. There is nothing to fix in those.

Lines starting Fatal: are ours, and there are five of them:

Line What stopped the run
Fatal: Execution trapped The instance died. A detail follows it where the engine did not already print one
Fatal: Execution Time Limit Exceeded The run passed its invocation budget
Fatal: Compute Budget Exceeded The run passed its compute budget
Fatal: Response Size Limit Exceeded The response grew past what one invocation may return
Fatal: No room to start the Function The host had no room to start the run, so the caller got a 503. Try again shortly

A Fatal: line is always the last line, because the run ended on it.

--follow on a Function name streams that Function’s output as it is produced, across runs.

npx wawesome logs charge --follow
[wawesome] 📡 Following function shop/charge (Ctrl-C to stop)...

Then it waits. Nothing more is printed until the Function is invoked, and it makes no difference whether a run was already going when you started. Hit the URL from another window and the lines arrive:

Log: charging 4210 for cus_9f2
Log: charged 4210 for cus_9f2

Following prints msg on its own, without the timestamp and the stream, and colors stderr lines red. Read the run back with --invocation afterwards for the timestamps.

The stream stays open across runs, so the next invocation prints under the last one. Ctrl-C stops it:

[wawesome] Stopped following.

The console follows a Function the same way. On its Invocations tab, Live output at the right of the bar swaps the rows for the output as it arrives. Switch it off and the rows come back as you left them.

You hold one live tail at a time across the whole platform. Open a second one anywhere and the first ends. Worth knowing before you go hunting for why a terminal you left open went quiet.

--follow with an invocation id follows that one run instead, and returns when it ends:

npx wawesome logs 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 --follow
[wawesome] 📡 Following invocation 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34 (Ctrl-C to stop)...
Log: charging 4210 for cus_9f2
[wawesome] ✔ Live tail ended for 6b1f9c2e-4d3a-4f8b-9c11-2a7e5d0b8f34.

A run that has already finished has nothing left to stream, so that prints the two [wawesome] lines and no output at all. Drop --follow to read a finished run.

invoke starts a run through the authenticated API and follows its output, without waiting for a caller or for a schedule to come round.

npx wawesome invoke charge
[wawesome] ⏳ Run 2f8c5b91-7e04-4a3d-8b6c-1d9f4e2a0c53 queued for 'shop/charge'...
Log: charging 4210 for cus_9f2
Log: charged 4210 for cus_9f2
[wawesome] ✔ Run completed: success (1.42s)

The run reaches your handler as a POST with no body. -m sets the method and -d sets the body:

npx wawesome invoke charge -m POST -d '{"customer":"cus_9f2"}'

--no-follow fires the run and exits on the queued line, leaving you the id to read later.

An invoked run is a background run, which is why it lists as manual rather than http. That also changes what the record says about it: a background run is success only when it answered 2xx, and error for anything else.

[wawesome] ✖ Run completed: error (38ms)

So one bug reads two ways. The 500 in the logs listing above is success on its http row and error on the manual row of the same handler failing the same way. Firing invoke at a handler you suspect is the quickest way to make a caller-facing 500 show up as a failure.

Your handler fails in two ways, and we record them differently. Telling them apart is the difference between one bad request and a Function that is answering nothing at all.

A throw inside your handler, or a promise it awaits that rejects, is caught by the engine. The engine answers 500 on your Function’s behalf, with an empty body.

export default {
  async fetch(request) {
    const customer = request.headers.get('x-customer');
    console.log(`charging 4210 for ${customer}`);

    const charge = await charges.create(customer, 4210);
    return Response.json(charge);
  },
};

The caller gets a 500 and no x-wawesome-error header. That absence is the signal. A status with no class beside it is your Function’s own answer, so we did not stop this run. On the wire it is indistinguishable from return new Response(null, { status: 500 }). The row in logs says success, because the run did answer.

The log body is where the two come apart:

{"ts":"2026-09-08T09:11:47.241902Z","stream":"stderr","msg":"Error while running request handler: charges is not defined"}
{"ts":"2026-09-08T09:11:47.241944Z","stream":"stderr","msg":"  fetch@./user.js:6:20"}

Your handler ran. It reached line 6 and threw. Catch it there and answer the status you mean, and the caller gets something more useful than an empty 500.

Do not answer 502 or 504. The caller gets a page that is not yours in their place, with neither your body nor your headers. When a service you call fails, answer 500.

Code outside the handler runs once, when the module is evaluated, before any request reaches your fetch. A throw there kills the instance.

const key = process.env.STRIPE_KEY;
if (!key) throw new Error('STRIPE_KEY is not set');

export default {
  async fetch(request) {
    return new Response('ok');
  },
};

The caller gets a 500 with x-wawesome-error: trap and the message The Function failed to run. The row in logs says error. The body carries the engine’s own account, and then our line under it:

{"ts":"2026-09-08T09:11:47.198004Z","stream":"stderr","msg":"Exception while evaluating top-level script"}
{"ts":"2026-09-08T09:11:47.198091Z","stream":"stderr","msg":"./user.js:2:17 Error: STRIPE_KEY is not set"}
{"ts":"2026-09-08T09:11:47.198119Z","stream":"stderr","msg":"Stack:"}
{"ts":"2026-09-08T09:11:47.198126Z","stream":"stderr","msg":"  @./user.js:2:17"}
{"ts":"2026-09-08T09:11:47.198140Z","stream":"stderr","msg":"Fatal: Execution trapped"}

Your handler never ran, and it will not run for the next request either. Every call fails the same way until you deploy a fix. A trap is an outage. A 500 is one request going wrong.

Only a module-level throw traps. Nothing you can call from inside a handler will, because no API we give you aborts the instance. A failure in there is always the 500 above.

Threw in the handler Threw at module scope
What the caller gets 500, empty body 500, The Function failed to run
x-wawesome-error absent trap
STATUS in logs success error
First line to read in the body Error while running request handler: Exception while evaluating top-level script
Requests affected this one every one, until you deploy

The third row is why --error is the wrong place to look for a 500 somebody reported. Ask them for the x-wawesome-invocation-id off their response and read that run directly.

The console does find them. On a Function’s Invocations tab, Failed lists every run that failed whoever asked: one that never answered, and one that answered 500 or above, whatever the trigger. A 4xx is not in it, because that is the Function saying the request was wrong. Open a row to read the last lines of its output, and All failures like this to see the rest.