Skip to content

Errors

MaSH treats an error as a fact to record, not as an event that unwinds the script. This is different from most languages, and everything on this page follows from it.

When a statement fails, MaSH records the error and goes on to the next statement. The script does not stop. There is no try and no catch.

set x to 5 / 0
printline "still running"
set y to nosuch()
printline "still running again"
still running
still running again

Both failures were recorded. Neither was shown, because nothing asked.

A statement that fails does as much as it can. A failed assignment gives the variable the value null (or NAN for a division by zero). A failed call gives null. A failed printline prints nothing, not even the line ending.

set z to 1
set z to z + nosuch.thing
printline mash.type(z)
printline "before"
printline {a: 1}.missing
printline "after"
NULL
before
after

The things that cause a recorded error and carry on include:

  • Dividing or taking a remainder by zero.
  • Calling a block that does not exist (Undefined block or function: name).
  • Calling a method that does not exist on a value (Unable to call method "upper" of non-object "string".).
  • Reading a missing key of a dictionary with a dot (Item "k" does not exist.).
  • Adding text to a number with +.
  • Anything an object refuses: a missing file, a failed web request, a permission.

throw and raise are different. They record an error and stop the block they are in. See throw and raise.

The last recorded error is available as mash.error.

PropertyValue
mash.error.statetrue when an error is recorded and not yet handled
mash.error.messagethe message
mash.error.linethe line of the failed statement, or empty
mash.error.expressionthe text of that line, or empty
set x to 5 / 0
printline mash.error.state
printline mash.error.message
printline mash.error.line
printline mash.error.expression
true
Division by zero
1
set x to 5 / 0

Only the last error is kept. A second error replaces the first.

The state is sticky. It stays true through later statements that succeed, until an error block handles it. Reading mash.error.state does not clear it.

An error block runs when an error is recorded and not yet handled. It is checked where you put it: after the statements you want to watch.

set data to 10 / 0
error
printline "caught: {{mash.error.message}}"
end
printline "carrying on"
caught: Division by zero
carrying on

An error block is if mash.error.state under another name. Both forms mean the same:

throw "first"
if mash.error.state
printline "explicit check: {{mash.error.message}}"
end
explicit check: first

When an error block begins, the error counts as handled, and when the block ends the error state is cleared. A second error block straight after finds nothing to do:

set x to 10 / 0
printline "step 1"
printline "step 2"
error
printline "handled: {{mash.error.message}}"
end
printline "state now: {{mash.error.state}}"
error
printline "never runs"
end
step 1
step 2
handled: Division by zero
state now: false

Notice that the block caught an error from two statements earlier. It does not look only at the line above. It looks at whatever was recorded since the last time one ran. To tell which statement failed, read mash.error.line and mash.error.expression.

An empty error block is the way to clear the state and ignore an error:

throw "ignore me"
error
end
printline mash.error.state
false

Inside the block you can use exit, next, break, return, fail and throw as anywhere else. exit ends the script. A throw inside the block records a new error, which is then the current one, and it behaves as a throw anywhere else: it ends the block the error block is in (the run, at top level).

set x to 1 / 0
error
printline "handling first"
throw "second"
end
printline "never printed"

If an if, while, until or ensure condition fails to compute, the error is recorded and the condition is read as false. The else branch runs.

set d to {a: 1}
if d.missing
printline "yes"
else
printline "no"
end
printline mash.error.message
no
Item "missing" does not exist.

Name a block as the global error block, and it runs after every statement that records an error, without you placing an error block each time.

block onError
printline "global handler: {{mash.error.message}}"
end
set errorblock to "onError"
set x to 5 % 0
printline "no error here"
set y to 1 / 0
global handler: Modulo by zero
no error here
global handler: Division by zero

errorblock = "name" and mash.error.block = "name" do the same. The handler counts the error as handled. If the handler itself fails or calls fail, the script stops.

The handler runs after incidental errors such as the division above, and the script carries on. After a throw it behaves differently: see below.

There is no way to switch it off again; errorblock = null does not.

throw "message" records an error and stops the block it is in. raise is the same word.

If an error block comes straight after the throw, it handles the error and the script carries on after the block.

throw "something odd"
error
printline "caught: {{mash.error.message}}"
end
printline "carrying on"
caught: something odd
carrying on

If no error block is straight after, the error is recorded and the global error block runs if there is one. Then the block the throw is in ends at once, however deep in if, each or while it was. A later error block in that same block does not run. The code that called the block then skips to its own next error block, which can be after the call or after a loop or if that holds the call. The statements in between do not run. At the top level of the script there is no caller, so a throw there, not caught straight after, ends the run.

block check
throw "bad input"
printline "never printed"
end
check()
printline "never printed either"
error
printline "caught: {{mash.error.message}}"
end
printline "end of script"
caught: bad input
end of script

If no caller catches it, the run ends and the run’s error is the message. This is so even when the global error block handled it.

printline "before"
throw "no handler"
printline "never printed"

throw with no message records an empty error. mash.error.line and mash.error.expression give the line of the throw.

fail "message" stops the script at once. The run is reported as failed, with the message, the source line and the line number. An error block cannot catch it, and nothing after it runs.

printline "before"
fail "input file missing"
printline "never printed"

A common shape is to handle the error, then decide:

set data to mash.files.get("/Reports/today.csv")
error
fail "Could not read today's report: {{mash.error.message}}"
end

exit ends the whole script and reports success, including when it is inside a block. See Statements.

stoponerror = true turns the language round. From that line on, any recorded error stops the script, the way an exception would elsewhere. The run is reported as failed with that error.

set stoponerror to true
printline "a"
throw "boom"
printline "b"

Put stoponerror = true at the top of a script where a silent partial run would do harm. It is the single most useful safety setting in the language. stoponerror = false returns to the default.

debug = true is accepted and has no effect on the script’s behaviour.

Some errors end the run whatever the script does. They are not recorded and carried on; they stop everything.

  • A syntax error. The script does not start. Nothing is run and nothing is printed.
  • Reading or calling a name on mash that does not exist, such as mash.nosuchthing.
  • Running out of time or memory. See Limits.
  • fail, a fail or an error inside the global error block, and stoponerror.
  • A throw with no error block after it.

A syntax error is reported with its line and column, and the script does not run.

MessageMeaning
Parse error at line 2, column 1: Unexpected token end (end)a word or symbol turned up where the language did not expect it
Syntax error at line 1, column 11: Unterminated stringa quote was never closed
Syntax error at line 1, column 13: Expected ), got as (as)one thing was required and another was found
`mash` is reserved and cannot be assigned. Rename the local.an assignment to a name the language owns
ensure must return, next, break, fail or exitan ensure with no way out
Unexpected character '&' — did you mean '&&'?a single & or |

A missing end at the very end of the script is accepted, so a script that forgets the last end runs as if it were there.

An error that nothing handles is still recorded, and the run keeps a list of them. For each distinct failing line the list holds the line, the text of the line, the message and a count. A loop that fails ten thousand times is one entry with a count of 10,000. At most 50 distinct entries are kept, and the total is always counted.

An error counts as handled, and leaves the list, when an error block or the global error block deals with it. So the list holds the errors that were swallowed.

Where it shows:

  • In the Mashup execution panel, as Unhandled errors (n), under the output. Each entry shows its line number, which you can click to jump to the line, the message, a count when it happened more than once, and the line of code.
  • In the run log on Settings > MaSH, as a warning count on the run, and as the full list when you open the run.

This was read from the interface code and is not a promise about its wording.

A run that stopped shows its error as the run’s error, in the form:

Message: input file missing
Source: fail "input file missing"
Line: 2

The two are different things. A run with unhandled errors finished all its statements. A run with an error stopped before the end. A run that finished is not reported as an error, so look at the unhandled list as well as the status.

A script that fetches data and checks the result at each step:

block loadRows
set rows to [1, 2, 3]
return rows
end
set rows to loadRows()
error
fail "Could not load rows: {{mash.error.message}}"
end
set total to 0
each rows as r
increment total by r
end
printline "total is {{total}}"
total is 6